Files
PVabel2026/RemoteFrameBuffer/Client/RemoteFrameBufferClientProtocol.cs
2026-02-20 12:23:23 +01:00

239 lines
9.5 KiB
C#

using RemoteFrameBuffer.Common;
using System.Data;
using System.Text;
namespace RemoteFrameBuffer.Client
{
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;
public abstract RfbProtoVersion Version { get; }
public RemoteFramebufferSecurityType SecurityType { get; protected set; } = RemoteFramebufferSecurityType.Invalid;
private RgbFrameBuffer? _frameBuffer;
public delegate void BitmapUpdateCallback(Byte[] bitmap);
public event BitmapUpdateCallback? FrameUpdate;
public RemoteFrameBufferClientProtocol(Stream s)
{
this.vncStream = s;
}
public abstract void Handshake(CancellationToken token);
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.FrameUpdate?.Invoke(this._frameBuffer!.ToBitmap());
//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();
}
}
}