It can actually take screenshots of the VM
This commit is contained in:
@@ -1,18 +1,234 @@
|
|||||||
using RemoteFrameBuffer.Common;
|
using RemoteFrameBuffer.Common;
|
||||||
|
using System.Data;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace RemoteFrameBuffer.Client
|
namespace RemoteFrameBuffer.Client
|
||||||
{
|
{
|
||||||
public abstract class RemoteFrameBufferClientProtocol
|
public abstract class RemoteFrameBufferClientProtocol
|
||||||
{
|
{
|
||||||
|
public enum RfbClientProtocolPhase
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Constructed,
|
||||||
|
Handshake,
|
||||||
|
HandshakeDone,
|
||||||
|
Initialization,
|
||||||
|
InitializationDone,
|
||||||
|
Listening,
|
||||||
|
Stopped
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task? backgroundWorker;
|
||||||
|
private CancellationTokenSource? cancellationTokenSource;
|
||||||
|
private Timer? _timer;
|
||||||
|
public RfbClientProtocolPhase Phase { get; protected set; } = RfbClientProtocolPhase.None;
|
||||||
|
|
||||||
protected readonly Stream vncStream;
|
protected readonly Stream vncStream;
|
||||||
public abstract RfbProtoVersion Version { get; }
|
public abstract RfbProtoVersion Version { get; }
|
||||||
|
public RemoteFramebufferSecurityType SecurityType { get; protected set; } = RemoteFramebufferSecurityType.Invalid;
|
||||||
|
|
||||||
|
private RgbFrameBuffer? _frameBuffer;
|
||||||
|
|
||||||
public RemoteFrameBufferClientProtocol(Stream s)
|
public RemoteFrameBufferClientProtocol(Stream s)
|
||||||
{
|
{
|
||||||
this.vncStream = s;
|
this.vncStream = s;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract void Handshake();
|
public abstract void Handshake(CancellationToken token);
|
||||||
public abstract void Initialization();
|
public void Initialization(CancellationToken token)
|
||||||
|
{
|
||||||
|
if (this.Phase != RfbClientProtocolPhase.HandshakeDone)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("In wrong phase when calling Initialization");
|
||||||
|
}
|
||||||
|
this.Phase = RfbClientProtocolPhase.Initialization;
|
||||||
|
Byte[] buf;
|
||||||
|
// ClientInit: Ask for a shared session; should probably make it configurable later
|
||||||
|
this.vncStream.WriteByte((Byte)0);
|
||||||
|
// ServerInit
|
||||||
|
buf = new Byte[24];
|
||||||
|
CancellationTokenSource readTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||||
|
CancellationTokenSource readTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token, readTimeout.Token);
|
||||||
|
this.vncStream.ReadExactlyAsync(buf, 0, buf.Length, readTokenSource.Token).AsTask().Wait();
|
||||||
|
if (BitConverter.IsLittleEndian)
|
||||||
|
{
|
||||||
|
Array.Reverse(buf, 0, 2);// fb width
|
||||||
|
Array.Reverse(buf, 2, 2);// fb height
|
||||||
|
Array.Reverse(buf, 8, 2);// red-max
|
||||||
|
Array.Reverse(buf, 10, 2);// green-max
|
||||||
|
Array.Reverse(buf, 12, 2);// blue-max
|
||||||
|
Array.Reverse(buf, 20, 4);// name mength
|
||||||
|
}
|
||||||
|
UInt16 width = BitConverter.ToUInt16(buf, 0);
|
||||||
|
UInt16 height = BitConverter.ToUInt16(buf, 2);
|
||||||
|
Console.Error.WriteLine($"FB: {width}x{height} {buf[5]}({buf[4]}) {(buf[6] == 0 ? "LE" : "BE")} {(buf[7] == 0 ? "pallette" : "TrueColor")}");
|
||||||
|
this._frameBuffer = new(width, height);
|
||||||
|
UInt32 nameLen = BitConverter.ToUInt32(buf, 20);
|
||||||
|
String name;
|
||||||
|
if (nameLen > 0)
|
||||||
|
{
|
||||||
|
buf = new Byte[nameLen];
|
||||||
|
readTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||||
|
readTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token, readTimeout.Token);
|
||||||
|
this.vncStream.ReadExactlyAsync(buf, 0, buf.Length, readTokenSource.Token).AsTask().Wait();
|
||||||
|
name = Encoding.UTF8.GetString(buf);
|
||||||
|
Console.Error.WriteLine($"Name: {name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Phase = RfbClientProtocolPhase.InitializationDone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartListener(CancellationToken token)
|
||||||
|
{
|
||||||
|
if (this.Phase == RfbClientProtocolPhase.InitializationDone)
|
||||||
|
{
|
||||||
|
this.cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token);
|
||||||
|
this.Phase = RfbClientProtocolPhase.Listening;
|
||||||
|
Console.Error.WriteLine("VNC client starting");
|
||||||
|
this.backgroundWorker = new Task(this.Listener, this.cancellationTokenSource.Token);
|
||||||
|
this.backgroundWorker.ContinueWith(this.ListenerStopped);
|
||||||
|
this.backgroundWorker.Start();
|
||||||
|
this._timer = new(this.FrameUpdateRequest, null, TimeSpan.FromMilliseconds(100), TimeSpan.FromMilliseconds(2000));
|
||||||
|
return this.backgroundWorker;
|
||||||
|
}else
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("In wrong phase when calling StartListener");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void FrameUpdateRequest(Object? si)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("Requesting update");
|
||||||
|
Byte[] w = BitConverter.GetBytes(this._frameBuffer!.Width);
|
||||||
|
Byte[] h = BitConverter.GetBytes(this._frameBuffer!.Height);
|
||||||
|
if (BitConverter.IsLittleEndian)
|
||||||
|
{
|
||||||
|
Array.Reverse(w);
|
||||||
|
Array.Reverse(h);
|
||||||
|
}
|
||||||
|
Byte[] buf = new Byte[]{ 3, 1, 0, 0, 0, 0, w[0], w[1], h[0], h[1] };
|
||||||
|
this.vncStream.Write(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ListenerStopped(Task t)
|
||||||
|
{
|
||||||
|
this.Phase = RfbClientProtocolPhase.Stopped;
|
||||||
|
Console.Error.WriteLine("VNC client exited");
|
||||||
|
this.backgroundWorker = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopListener()
|
||||||
|
{
|
||||||
|
if (this.Phase != RfbClientProtocolPhase.Listening)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("In wrong phase when calling StopListener");
|
||||||
|
}
|
||||||
|
if (this.backgroundWorker != null)
|
||||||
|
{
|
||||||
|
this.cancellationTokenSource!.Cancel();
|
||||||
|
this.backgroundWorker.Wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Listener()
|
||||||
|
{
|
||||||
|
Byte[] buf = new Byte[1];
|
||||||
|
while (!this.cancellationTokenSource!.Token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
this.vncStream.ReadExactlyAsync(buf, 0, buf.Length, this.cancellationTokenSource.Token).AsTask().Wait();
|
||||||
|
//Console.Error.WriteLine($"<== ServerMessage {buf[0]}");
|
||||||
|
this.HandleServerMessage(buf[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void HandleServerMessage(Byte code)
|
||||||
|
{
|
||||||
|
switch (code)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
this.ServerMsg_FramebufferUpdate();
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
this.ServerMsg_SetColourMapEntries();
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
this.ServerMsg_Bell();
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
this.ServerMsg_ServerCutText();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new NotSupportedException($"Unknown server message ID: {code}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void ServerMsg_FramebufferUpdate()
|
||||||
|
{
|
||||||
|
Byte[] buf = new Byte[3];
|
||||||
|
this.vncStream.ReadExactlyAsync(buf, 0, buf.Length, this.cancellationTokenSource!.Token).AsTask().Wait();
|
||||||
|
if (BitConverter.IsLittleEndian)
|
||||||
|
{
|
||||||
|
Array.Reverse(buf, 1, 2);
|
||||||
|
}
|
||||||
|
UInt16 rectangles = BitConverter.ToUInt16(buf, 1);
|
||||||
|
Console.Error.WriteLine($"<<= FramebufferUpdate ({rectangles})");
|
||||||
|
for (Int32 i = 0; i < rectangles; i++)
|
||||||
|
{
|
||||||
|
buf = new Byte[12];
|
||||||
|
this.vncStream.ReadExactlyAsync(buf, 0, buf.Length, this.cancellationTokenSource!.Token).AsTask().Wait();
|
||||||
|
if (BitConverter.IsLittleEndian)
|
||||||
|
{
|
||||||
|
Array.Reverse(buf, 0, 2);
|
||||||
|
Array.Reverse(buf, 2, 2);
|
||||||
|
Array.Reverse(buf, 4, 2);
|
||||||
|
Array.Reverse(buf, 6, 2);
|
||||||
|
Array.Reverse(buf, 8, 4);
|
||||||
|
}
|
||||||
|
UInt16 x = BitConverter.ToUInt16(buf,0);
|
||||||
|
UInt16 y = BitConverter.ToUInt16(buf,2);
|
||||||
|
UInt16 w = BitConverter.ToUInt16(buf,4);
|
||||||
|
UInt16 h = BitConverter.ToUInt16(buf,6);
|
||||||
|
UInt32 encoding = BitConverter.ToUInt32(buf,8);
|
||||||
|
Console.Error.WriteLine($"RECT {w}x{h} @ {x}:{y}, as {encoding}");
|
||||||
|
switch (encoding)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
#warning I'm straight up assuming 24-bit little endian RGB sent as padded 32bit pixels, ignoring what format the server specified
|
||||||
|
Int32 bytes = w * h * 4;
|
||||||
|
buf = new Byte[bytes];
|
||||||
|
this.vncStream.ReadExactlyAsync(buf, 0, buf.Length, this.cancellationTokenSource!.Token).AsTask().Wait();
|
||||||
|
RgbPixel[][] px = new RgbPixel[h][];
|
||||||
|
for (Int32 row = 0; row < h; row++)
|
||||||
|
{
|
||||||
|
px[row] = new RgbPixel[w];
|
||||||
|
for (Int32 col = 0; col < w; col++)
|
||||||
|
{
|
||||||
|
Int32 off = (row*w*4)+(col*4);
|
||||||
|
px[row][col] = new RgbPixel() { Red = buf[off+2], Green = buf[off+1], Blue = buf[off] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._frameBuffer!.SetRegion(px, 0, 0, w, h, x, y);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._frameBuffer!.SaveBitmap($"{DateTime.Now:yyyyMMdd-HHmmss}.bmp");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void ServerMsg_SetColourMapEntries()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void ServerMsg_Bell()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void ServerMsg_ServerCutText()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using RemoteFrameBuffer.Common;
|
using RemoteFrameBuffer.Common;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace RemoteFrameBuffer.Client
|
namespace RemoteFrameBuffer.Client
|
||||||
{
|
{
|
||||||
@@ -8,15 +9,42 @@ namespace RemoteFrameBuffer.Client
|
|||||||
|
|
||||||
public RemoteFrameBufferClientProtocol_3_3(Stream s) : base(s)
|
public RemoteFrameBufferClientProtocol_3_3(Stream s) : base(s)
|
||||||
{
|
{
|
||||||
|
this.Phase = RfbClientProtocolPhase.Constructed;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Handshake()
|
public override void Handshake(CancellationToken token)
|
||||||
{
|
{
|
||||||
|
if (this.Phase != RfbClientProtocolPhase.Constructed)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("In wrong phase when calling Handshake");
|
||||||
}
|
}
|
||||||
|
this.Phase = RfbClientProtocolPhase.Handshake;
|
||||||
public override void Initialization()
|
Byte[] buf;
|
||||||
|
// Request protocol version
|
||||||
|
this.vncStream.Write(Encoding.ASCII.GetBytes("RFB 003.003\n"));
|
||||||
|
// Read security type
|
||||||
|
buf = new Byte[4];
|
||||||
|
CancellationTokenSource readTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||||
|
CancellationTokenSource readTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token, readTimeout.Token);
|
||||||
|
this.vncStream.ReadExactlyAsync(buf, 0, buf.Length, readTokenSource.Token).AsTask().Wait();
|
||||||
|
if (BitConverter.IsLittleEndian)
|
||||||
{
|
{
|
||||||
|
Array.Reverse(buf);
|
||||||
|
}
|
||||||
|
this.SecurityType = (RemoteFramebufferSecurityType)BitConverter.ToUInt32(buf, 0);
|
||||||
|
Console.Error.WriteLine($"Server uses security type {this.SecurityType}");
|
||||||
|
switch (this.SecurityType)
|
||||||
|
{
|
||||||
|
case RemoteFramebufferSecurityType.Invalid:
|
||||||
|
throw new Exception("Connection failed");
|
||||||
|
case RemoteFramebufferSecurityType.None:
|
||||||
|
break;// RFB 3.3 straight up jumps to initialization here
|
||||||
|
case RemoteFramebufferSecurityType.VncAuthentication:
|
||||||
|
throw new NotImplementedException("VNC Authentication is not implemented");
|
||||||
|
default:
|
||||||
|
throw new NotSupportedException("RFB version 3.3 doesn't support the security type returned by the server");
|
||||||
|
}
|
||||||
|
this.Phase = RfbClientProtocolPhase.HandshakeDone;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,13 +84,21 @@ namespace RemoteFrameBuffer
|
|||||||
throw new Exception("Cannot parse protocol version");
|
throw new Exception("Cannot parse protocol version");
|
||||||
}
|
}
|
||||||
RfbProtoVersion serverVersion = new(Int16.Parse(m.Groups["major"].Value), Int16.Parse(m.Groups["minor"].Value));
|
RfbProtoVersion serverVersion = new(Int16.Parse(m.Groups["major"].Value), Int16.Parse(m.Groups["minor"].Value));
|
||||||
RfbProtoVersion maxProto = _protocolHandlers.Keys.Where((ph) => ph <= serverVersion).Max();
|
RfbProtoVersion[] supportedProto = _protocolHandlers.Keys.Where((ph) => ph <= serverVersion).ToArray();
|
||||||
|
if (supportedProto.Length == 0)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException("No common protocol between client and server");
|
||||||
|
}
|
||||||
|
RfbProtoVersion maxProto = supportedProto.Max();
|
||||||
RemoteFrameBufferClientProtocol proto = _protocolHandlers[maxProto].Construct(s);
|
RemoteFrameBufferClientProtocol proto = _protocolHandlers[maxProto].Construct(s);
|
||||||
Console.Error.WriteLine($"Using client protocol {proto.Version.Major}.{proto.Version.Minor}");
|
Console.Error.WriteLine($"Using client protocol {proto.Version.Major}.{proto.Version.Minor}");
|
||||||
proto.Handshake();
|
// TODO: register auth callback and the like
|
||||||
proto.Initialization();
|
proto.Handshake(this.cancellationTokenSource.Token);
|
||||||
|
proto.Initialization(this.cancellationTokenSource.Token);
|
||||||
|
proto.StartListener(this.cancellationTokenSource.Token).Wait();
|
||||||
}
|
}
|
||||||
|
#warning REMOVE THIS
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace RemoteFrameBuffer.Common
|
||||||
|
{
|
||||||
|
public enum RemoteFramebufferSecurityType
|
||||||
|
{
|
||||||
|
Invalid = 0,
|
||||||
|
None = 1,
|
||||||
|
VncAuthentication = 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@
|
|||||||
public Int16 Major = major;
|
public Int16 Major = major;
|
||||||
public Int16 Minor = minor;
|
public Int16 Minor = minor;
|
||||||
|
|
||||||
|
public override Boolean Equals(Object? obj) => obj is RfbProtoVersion version && this.Major == version.Major && this.Minor == version.Minor;
|
||||||
|
public override Int32 GetHashCode() => HashCode.Combine(this.Major, this.Minor);
|
||||||
|
|
||||||
public static Boolean operator ==(RfbProtoVersion a, RfbProtoVersion b)
|
public static Boolean operator ==(RfbProtoVersion a, RfbProtoVersion b)
|
||||||
{
|
{
|
||||||
@@ -30,11 +32,11 @@
|
|||||||
}
|
}
|
||||||
public static Boolean operator <=(RfbProtoVersion a, RfbProtoVersion b)
|
public static Boolean operator <=(RfbProtoVersion a, RfbProtoVersion b)
|
||||||
{
|
{
|
||||||
return b < a;
|
return !(b < a);
|
||||||
}
|
}
|
||||||
public static Boolean operator >=(RfbProtoVersion a, RfbProtoVersion b)
|
public static Boolean operator >=(RfbProtoVersion a, RfbProtoVersion b)
|
||||||
{
|
{
|
||||||
return a < b;
|
return !(a < b);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
78
RemoteFrameBuffer/Common/RgbFrameBuffer.cs
Normal file
78
RemoteFrameBuffer/Common/RgbFrameBuffer.cs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
namespace RemoteFrameBuffer.Common
|
||||||
|
{
|
||||||
|
public class RgbFrameBuffer
|
||||||
|
{
|
||||||
|
private RgbPixel[][] _frame;
|
||||||
|
|
||||||
|
public UInt16 Width
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return (UInt16)this._frame[0].Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public UInt16 Height
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return (UInt16)this._frame.Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RgbPixel[][] MakeFrameBuffer(UInt16 width, UInt16 height)
|
||||||
|
{
|
||||||
|
RgbPixel[][] buf = new RgbPixel[height][];
|
||||||
|
for (Int32 i = 0; i < height; i++)
|
||||||
|
{
|
||||||
|
buf[i] = new RgbPixel[width];
|
||||||
|
}
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RgbFrameBuffer(UInt16 width, UInt16 height)
|
||||||
|
{
|
||||||
|
this._frame = MakeFrameBuffer(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetRegion(RgbPixel[][] source, UInt16 source_x, UInt16 source_y, UInt16 width, UInt16 height, UInt16 target_x, UInt16 target_y)
|
||||||
|
{
|
||||||
|
for (Int32 y = 0; y < height; y++)
|
||||||
|
{
|
||||||
|
Array.Copy(source[y + source_y], source_x, this._frame[y + target_y], target_x, width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CopyRegion(UInt16 source_x, UInt16 source_y, UInt16 width, UInt16 height, UInt16 target_x, UInt16 target_y) => this.SetRegion(this._frame, source_x, source_y, width, height, target_x, target_y);
|
||||||
|
|
||||||
|
public void SaveBitmap(String filename)
|
||||||
|
{
|
||||||
|
#warning Endianness is straight up ignored
|
||||||
|
Int32 rowBytes = (this.Width * 3) + 3;
|
||||||
|
rowBytes -= rowBytes % 4;
|
||||||
|
Int32 bytes = 54 + (this.Height * rowBytes);
|
||||||
|
Byte[] buf = new Byte[bytes];
|
||||||
|
buf[0] = 0x42;
|
||||||
|
buf[1] = 0x4D;
|
||||||
|
Array.Copy(BitConverter.GetBytes(bytes), 0, buf, 2, 4);
|
||||||
|
buf[10] = 54;
|
||||||
|
buf[14] = 40;
|
||||||
|
Array.Copy(BitConverter.GetBytes((UInt32)this.Width), 0, buf, 18, 4);
|
||||||
|
Array.Copy(BitConverter.GetBytes((UInt32)this.Height), 0, buf, 22, 4);
|
||||||
|
buf[26] = 1;
|
||||||
|
buf[28] = 24;
|
||||||
|
Array.Copy(BitConverter.GetBytes(this.Height * rowBytes), 0, buf, 34, 4);
|
||||||
|
for (Int32 j = 0; j < this.Height; j++)
|
||||||
|
{
|
||||||
|
Int32 off = 54 + (j * rowBytes);
|
||||||
|
for (Int32 i = 0; i < this.Width; i++)
|
||||||
|
{
|
||||||
|
Int32 pos = off + (i * 3);
|
||||||
|
buf[pos] = this._frame[this.Height - j - 1][i].Blue;
|
||||||
|
buf[pos + 1] = this._frame[this.Height - j - 1][i].Green;
|
||||||
|
buf[pos + 2] = this._frame[this.Height - j - 1][i].Red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
File.WriteAllBytes(filename, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
RemoteFrameBuffer/Common/RgbPixel.cs
Normal file
9
RemoteFrameBuffer/Common/RgbPixel.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace RemoteFrameBuffer.Common
|
||||||
|
{
|
||||||
|
public struct RgbPixel
|
||||||
|
{
|
||||||
|
public Byte Red { get; set; }
|
||||||
|
public Byte Green { get; set; }
|
||||||
|
public Byte Blue { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user