268 lines
7.5 KiB
C#
268 lines
7.5 KiB
C#
|
namespace Timelapse.Desktop;
|
||
|
|
||
|
using System;
|
||
|
using System.Net;
|
||
|
using System.Net.Sockets;
|
||
|
using System.Reflection;
|
||
|
using System.Text;
|
||
|
using Eto.Forms;
|
||
|
using FlashCap;
|
||
|
using FlashCap.Devices;
|
||
|
using FlashCap.Utilities;
|
||
|
using SixLabors.ImageSharp;
|
||
|
using SixLabors.ImageSharp.PixelFormats;
|
||
|
using Tesses.WebServer;
|
||
|
|
||
|
public class proxsvr : Tesses.WebServer.IServer
|
||
|
{
|
||
|
public proxsvr(IServer svr)
|
||
|
{
|
||
|
dest=svr;
|
||
|
}
|
||
|
IServer dest;
|
||
|
public void AddCors(ServerContext ctx)
|
||
|
{
|
||
|
|
||
|
dest.AddCors(ctx);
|
||
|
}
|
||
|
|
||
|
public async Task<bool> BeforeAsync(ServerContext ctx)
|
||
|
{
|
||
|
return await dest.BeforeAsync(ctx);
|
||
|
}
|
||
|
|
||
|
public async Task GetAsync(ServerContext ctx)
|
||
|
{
|
||
|
await dest.GetAsync(ctx);
|
||
|
if(ctx.RawUrl != "/api/stream.jpg")
|
||
|
{
|
||
|
await ctx.NetworkStream.DisposeAsync();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public async Task OptionsAsync(ServerContext ctx)
|
||
|
{
|
||
|
await dest.OptionsAsync(ctx);
|
||
|
await ctx.NetworkStream.DisposeAsync();
|
||
|
}
|
||
|
|
||
|
public async Task OtherAsync(ServerContext ctx)
|
||
|
{
|
||
|
await dest.OtherAsync(ctx);
|
||
|
await ctx.NetworkStream.DisposeAsync();
|
||
|
}
|
||
|
|
||
|
public async Task PostAsync(ServerContext ctx)
|
||
|
{
|
||
|
await dest.PostAsync(ctx);
|
||
|
await ctx.NetworkStream.DisposeAsync();
|
||
|
}
|
||
|
}
|
||
|
public class EmbeadedServer : Server
|
||
|
{
|
||
|
|
||
|
public EmbeadedServer(string extname)
|
||
|
{
|
||
|
this.extname=extname;
|
||
|
}
|
||
|
string extname;
|
||
|
|
||
|
public override async Task GetAsync(ServerContext ctx)
|
||
|
{
|
||
|
var asm=Assembly.GetExecutingAssembly();
|
||
|
|
||
|
if(asm != null)
|
||
|
{
|
||
|
string j=$"{extname}{ctx.UrlPath.Replace('/','.')}";
|
||
|
var strm=asm.GetManifestResourceStream(j);
|
||
|
if(strm != null)
|
||
|
{
|
||
|
await ctx.SendStreamAsync(strm,HeyRed.Mime.MimeTypesMap.GetMimeType(ctx.UrlPath));
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
j=j.TrimEnd('.') + ".index.html";
|
||
|
strm=asm.GetManifestResourceStream(j);
|
||
|
if(strm != null)
|
||
|
{
|
||
|
await ctx.SendStreamAsync(strm,HeyRed.Mime.MimeTypesMap.GetMimeType("index.html"));
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
await NotFoundServer.ServerNull.GetAsync(ctx);
|
||
|
}
|
||
|
}
|
||
|
public class TimelapseWebServer
|
||
|
{
|
||
|
MainForm frm;
|
||
|
public TimelapseWebServer(MainForm frm)
|
||
|
{
|
||
|
this.frm=frm;
|
||
|
}
|
||
|
CancellationTokenSource? tokenSrc;
|
||
|
TcpListener? _listener;
|
||
|
|
||
|
private async Task CommunicateHostAsync(HttpServerListener listener,TcpClient clt)
|
||
|
{
|
||
|
try{
|
||
|
//<METHOD> <PATH> HTTP/1.1\r\n
|
||
|
//HEADER1\r\n
|
||
|
//HEADER2\r\n
|
||
|
//......
|
||
|
//HEADERN\r\n
|
||
|
//\r\n
|
||
|
//OPTIONAL REQUEST BODY
|
||
|
|
||
|
//RESPONSE
|
||
|
|
||
|
var strm = listener.GetStream(clt);
|
||
|
|
||
|
|
||
|
await listener.PushAsync(strm,clt.Client.LocalEndPoint,clt.Client.RemoteEndPoint);
|
||
|
|
||
|
}catch(Exception ex)
|
||
|
{
|
||
|
_=ex;
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
public async Task ListenAsync(HttpServerListener svr,CancellationToken token,Action<IPEndPoint> endpoint)
|
||
|
{
|
||
|
if(_listener == null) return;
|
||
|
_listener.Start();
|
||
|
if(endpoint != null)
|
||
|
{
|
||
|
endpoint((IPEndPoint)_listener.LocalEndpoint);
|
||
|
}
|
||
|
using (var r = token.Register(() => _listener.Stop())) {
|
||
|
while (!token.IsCancellationRequested)
|
||
|
{
|
||
|
try{
|
||
|
var socket=await _listener.AcceptTcpClientAsync();
|
||
|
await CommunicateHostAsync(svr,socket).ConfigureAwait(false);
|
||
|
}catch(Exception ex)
|
||
|
{
|
||
|
_=ex;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
public async void Listen()
|
||
|
{
|
||
|
if(tokenSrc != null)
|
||
|
{
|
||
|
tokenSrc.Cancel();
|
||
|
tokenSrc.Dispose();
|
||
|
|
||
|
}
|
||
|
if(!frm.Instance.Model.enableWebServer) return;
|
||
|
tokenSrc=new CancellationTokenSource();
|
||
|
//#if (!DEBUG)
|
||
|
EmbeadedServer esvr=new EmbeadedServer("Timelapse.ServerFiles");
|
||
|
// #else
|
||
|
// StaticServer esvr=new StaticServer("ServerFiles");
|
||
|
//#endif
|
||
|
RouteServer rs=new RouteServer();
|
||
|
rs.Add("/stream.jpg",StreamImage);
|
||
|
rs.Add("/state.json",State);
|
||
|
rs.Add("/setstate.cgi",SetState);
|
||
|
MountableServer isvr=new MountableServer(esvr);
|
||
|
isvr.Mount("/api/",rs);var p=new System.Net.IPEndPoint(System.Net.IPAddress.Any,frm.Instance.Model.timelapsePort);
|
||
|
_listener=new TcpListener(p);
|
||
|
HttpServerListener svr=new HttpServerListener(p,new proxsvr(isvr));
|
||
|
Console.WriteLine("Almost ready to listen");
|
||
|
await ListenAsync(svr,tokenSrc.Token,(e)=>{
|
||
|
int port=e.Port;
|
||
|
string dns=Dns.GetHostName();
|
||
|
|
||
|
var localIPs =string.Join('\n', Dns.GetHostAddresses(dns).Where(e=>{return !IPAddress.IsLoopback(e);}).Select<IPAddress,string>(e=>{
|
||
|
return $"http://{e.ToString()}:{port}/";
|
||
|
}));
|
||
|
if(frm.synchContext != null)
|
||
|
frm.synchContext.Post((e)=>{
|
||
|
|
||
|
|
||
|
MessageBox.Show($"Listening on:\n{localIPs}\nhttp://localhost:{port}\nhttp://{dns}:{port}/",MessageBoxButtons.OK);
|
||
|
},null);
|
||
|
});
|
||
|
|
||
|
}
|
||
|
|
||
|
private async Task SetState(ServerContext ctx)
|
||
|
{
|
||
|
string rec;
|
||
|
if(ctx.QueryParams.TryGetFirst("rec",out rec))
|
||
|
{
|
||
|
frm.Instance.Recording= rec != "false";
|
||
|
}
|
||
|
string real;
|
||
|
if(ctx.QueryParams.TryGetFirst("real",out real))
|
||
|
{
|
||
|
frm.Instance.RealTime= real != "false";
|
||
|
}
|
||
|
await ctx.SendRedirectAsync("./state.json");
|
||
|
|
||
|
}
|
||
|
|
||
|
private async Task State(ServerContext ctx)
|
||
|
{
|
||
|
await ctx.SendJsonAsync(new{Recording=frm.Instance.Recording,RealTime=frm.Instance.RealTime});
|
||
|
}
|
||
|
|
||
|
private byte[] GetBytes(Image<Rgb24> img)
|
||
|
{
|
||
|
using(var ms=new MemoryStream())
|
||
|
{
|
||
|
img.SaveAsJpeg(ms);
|
||
|
return ms.ToArray();
|
||
|
}
|
||
|
}
|
||
|
private async Task StreamImage(ServerContext ctx)
|
||
|
{
|
||
|
ctx.ResponseHeaders.Add("Content-Type","multipart/x-mixed-replace; boundary=--boundary");
|
||
|
await ctx.WriteHeadersAsync();
|
||
|
frm.NewFrame += (sender,e)=>{
|
||
|
try{
|
||
|
var img=e.Image.Clone();
|
||
|
Thread t =new Thread(()=>{
|
||
|
|
||
|
|
||
|
try{
|
||
|
lock(ctx){
|
||
|
StringBuilder b = new StringBuilder();
|
||
|
byte[] data = GetBytes(img);
|
||
|
img.Dispose();
|
||
|
b.Append("\r\n");
|
||
|
b.Append("--boundary\r\n");
|
||
|
b.Append("Content-Type: image/jpeg\r\n");
|
||
|
b.Append($"Content-Length: {data.Length}\r\n");
|
||
|
b.Append("\r\n");
|
||
|
|
||
|
byte[] hdr= Encoding.ASCII.GetBytes(b.ToString());
|
||
|
byte[] crlf = Encoding.ASCII.GetBytes("\r\n");
|
||
|
ctx.NetworkStream.Write(hdr,0,hdr.Length);
|
||
|
ctx.NetworkStream.Write(data,0,data.Length);
|
||
|
ctx.NetworkStream.Write(crlf,0,crlf.Length);
|
||
|
ctx.NetworkStream.Flush();
|
||
|
}
|
||
|
|
||
|
}catch(Exception ex)
|
||
|
{
|
||
|
try{
|
||
|
ctx.NetworkStream.Dispose();
|
||
|
}catch(Exception ex2)
|
||
|
{
|
||
|
_=ex2;
|
||
|
}
|
||
|
_=ex;
|
||
|
}
|
||
|
});
|
||
|
t.Start();
|
||
|
}catch(Exception ex)
|
||
|
{
|
||
|
_=ex;
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
}
|