433 lines
14 KiB
C#
433 lines
14 KiB
C#
|
using System.Net;
|
|||
|
using System.Net.Sockets;
|
|||
|
using Eto.Drawing;
|
|||
|
using Eto.Forms;
|
|||
|
using FlashCap;
|
|||
|
using FlashCap.Utilities;
|
|||
|
using SixLabors.ImageSharp;
|
|||
|
using SixLabors.ImageSharp.PixelFormats;
|
|||
|
using Timelapse.Desktop;
|
|||
|
using TimelapseApi;
|
|||
|
using Image = SixLabors.ImageSharp.Image;
|
|||
|
using System.Reflection;
|
|||
|
|
|||
|
public class MainForm : Form
|
|||
|
{
|
|||
|
TimelapseWebServer? svr;
|
|||
|
string[] args;
|
|||
|
public GuiData Gui {get;set;}
|
|||
|
public Api Instance {get;set;}
|
|||
|
|
|||
|
public SynchronizationContext? synchContext;
|
|||
|
|
|||
|
// Constructed capture device.
|
|||
|
private CaptureDevice? captureDevice;
|
|||
|
private PictureBox d;
|
|||
|
private CheckCommand record;
|
|||
|
private CheckCommand oneX;
|
|||
|
public MainForm(string[] args)
|
|||
|
{
|
|||
|
var asm=Assembly.GetExecutingAssembly();
|
|||
|
|
|||
|
var strm=asm.GetManifestResourceStream("Timelapse.ServerFiles.favicon.ico");
|
|||
|
if(strm != null)
|
|||
|
{
|
|||
|
this.Icon = new Icon(strm);
|
|||
|
}
|
|||
|
|
|||
|
record=new CheckCommand{
|
|||
|
MenuText="&Record",
|
|||
|
ToolBarText="Record"
|
|||
|
};
|
|||
|
|
|||
|
oneX = new CheckCommand{
|
|||
|
MenuText="&Real Time",
|
|||
|
ToolBarText="Real Time"
|
|||
|
};
|
|||
|
this.args=args;
|
|||
|
Gui=new GuiData();
|
|||
|
|
|||
|
|
|||
|
|
|||
|
Instance=new Api(Gui);
|
|||
|
Gui.Instance=Instance;
|
|||
|
Gui.CurrentFSIndex=0;
|
|||
|
Gui.Set().Wait();
|
|||
|
d=new PictureBox();
|
|||
|
}
|
|||
|
|
|||
|
public void CreateMenu()
|
|||
|
{
|
|||
|
Command close = new Command((sender,e)=>{Environment.Exit(0);});
|
|||
|
close.MenuText="&Exit";
|
|||
|
|
|||
|
Instance.RealTimeChanged += RealTime_Changed;
|
|||
|
Instance.RecordingChanged += Recording_Changed;
|
|||
|
Instance.ProjectOpened += ProjectOpened;
|
|||
|
Instance.ProjectClosed += ProjectClosed;
|
|||
|
record.CheckedChanged += Record_CheckedChanged;
|
|||
|
oneX.CheckedChanged += RealTime_CheckedChanged;
|
|||
|
NewFrame += NewFrameEvent;
|
|||
|
Command export=new Command(async(sender,e)=>{
|
|||
|
using(ExportWindow window = new ExportWindow(this,Instance,Gui,false))
|
|||
|
{
|
|||
|
await window.ShowModalAsync(this);
|
|||
|
}
|
|||
|
}){
|
|||
|
MenuText="&Export Project",
|
|||
|
ToolBarText="Export"
|
|||
|
};
|
|||
|
Command share = new Command(async(sender,e)=>{
|
|||
|
using(ExportWindow window = new ExportWindow(this,Instance,Gui,true))
|
|||
|
{
|
|||
|
await window.ShowModalAsync(this);
|
|||
|
}
|
|||
|
}){
|
|||
|
MenuText="&Share Project",
|
|||
|
ToolBarText="Share"
|
|||
|
};
|
|||
|
Command changeProjectSettings = new Command(async(sender,e)=>{
|
|||
|
var p = Instance.Project;
|
|||
|
if(p != null)
|
|||
|
{
|
|||
|
Instance.Project=null;
|
|||
|
using(TimelapseProjectSettings settings=new TimelapseProjectSettings(p))
|
|||
|
await settings.ShowModalAsync(this);
|
|||
|
|
|||
|
p.Save();
|
|||
|
Instance.Project=p;
|
|||
|
}
|
|||
|
}){
|
|||
|
MenuText="&Project Settings",
|
|||
|
ToolBarText="Project"
|
|||
|
|
|||
|
};
|
|||
|
|
|||
|
|
|||
|
Command newProject=new Command(async(sender,e)=>{
|
|||
|
|
|||
|
var cfs=Gui.CurrentFileSystem;
|
|||
|
if(cfs != null)
|
|||
|
{
|
|||
|
string? path=cfs.ShowSaveDialog(this,new FileFilter[]{new FileFilter("Timelapse Project",".tlnp")});
|
|||
|
if(!string.IsNullOrWhiteSpace(path))
|
|||
|
{
|
|||
|
string? changed=Path.ChangeExtension(path,"");
|
|||
|
if(!string.IsNullOrWhiteSpace(changed))
|
|||
|
{
|
|||
|
var ss= cfs+changed.Substring(0,changed.Length-1);
|
|||
|
if(ss != null)
|
|||
|
{
|
|||
|
|
|||
|
var proj=new TimelapseProject();
|
|||
|
|
|||
|
proj.ProjectLocation=ss;
|
|||
|
using(TimelapseProjectSettings settings=new TimelapseProjectSettings(proj))
|
|||
|
await settings.ShowModalAsync(this);
|
|||
|
proj.Save();
|
|||
|
Instance.Project = proj;
|
|||
|
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
}){
|
|||
|
MenuText="&New Project",
|
|||
|
ToolBarText="New"
|
|||
|
};
|
|||
|
|
|||
|
Command openProject=new Command((sender,e)=>{
|
|||
|
var cfs=Gui.CurrentFileSystem;
|
|||
|
if(cfs != null)
|
|||
|
{
|
|||
|
string? path=cfs.ShowOpenDialog(this,new FileFilter[]{new FileFilter("Timelapse Project",".tlnp")});
|
|||
|
if(!string.IsNullOrWhiteSpace(path))
|
|||
|
{
|
|||
|
if(cfs.FileExists(path))
|
|||
|
{
|
|||
|
|
|||
|
string? changed=Path.ChangeExtension(path,"");
|
|||
|
if(!string.IsNullOrWhiteSpace(changed))
|
|||
|
{
|
|||
|
var ss= cfs+changed.Substring(0,changed.Length-1);
|
|||
|
if(ss != null)
|
|||
|
{
|
|||
|
Instance.Project = TimelapseProject.Open(ss);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}){
|
|||
|
MenuText="&Open Project",
|
|||
|
ToolBarText="Open"
|
|||
|
};
|
|||
|
|
|||
|
|
|||
|
|
|||
|
var FsItem = new ButtonMenuItem {
|
|||
|
Text="File System",
|
|||
|
};
|
|||
|
var gfs = Gui.FileSystems;
|
|||
|
if(gfs != null){
|
|||
|
int i=0;
|
|||
|
foreach(var fs in gfs)
|
|||
|
{
|
|||
|
int j=i++;
|
|||
|
var rb =new RadioMenuItem();
|
|||
|
rb.Text=fs.Text;
|
|||
|
rb.CheckedChanged+=(sender,e)=>{
|
|||
|
if(rb.Checked)
|
|||
|
{
|
|||
|
Gui.CurrentFSIndex=j;
|
|||
|
Gui.FSChanged();
|
|||
|
}
|
|||
|
};
|
|||
|
FsItem.Items.Add(rb);
|
|||
|
}
|
|||
|
}
|
|||
|
var ExtSettings = new ButtonMenuItem {
|
|||
|
Text="Extension Settings"
|
|||
|
};
|
|||
|
var exts = Gui.ExtensionSettings;
|
|||
|
Dictionary<TimelapseExtension,List<ButtonMenuItem>> extBtns=new Dictionary<TimelapseExtension, List<ButtonMenuItem>>();
|
|||
|
if(exts != null)
|
|||
|
{
|
|||
|
foreach(var item in exts)
|
|||
|
{
|
|||
|
if(!extBtns.ContainsKey(item.Extension))
|
|||
|
{
|
|||
|
extBtns.Add(item.Extension,new List<ButtonMenuItem>());
|
|||
|
}
|
|||
|
var btn=new ButtonMenuItem{Text=item.Text};
|
|||
|
btn.Click += async(sender,e)=>{
|
|||
|
try{
|
|||
|
using(var dlg = item.Dialog())
|
|||
|
{
|
|||
|
await dlg.ShowModalAsync(this);
|
|||
|
}
|
|||
|
}catch(Exception ex)
|
|||
|
{
|
|||
|
_=ex;
|
|||
|
}
|
|||
|
};
|
|||
|
extBtns[item.Extension].Add(btn);
|
|||
|
}
|
|||
|
foreach(var item in extBtns)
|
|||
|
{
|
|||
|
var btn = new ButtonMenuItem() {Text=item.Key.Name};
|
|||
|
foreach(var item2 in item.Value)
|
|||
|
{
|
|||
|
btn.Items.Add(item2);
|
|||
|
}
|
|||
|
ExtSettings.Items.Add(btn);
|
|||
|
}
|
|||
|
}
|
|||
|
ToolBar = new ToolBar{
|
|||
|
Items = {
|
|||
|
new ButtonToolItem(newProject),
|
|||
|
new ButtonToolItem(openProject),
|
|||
|
new SeparatorToolItem(),
|
|||
|
new ButtonToolItem(export),
|
|||
|
new ButtonToolItem(share),
|
|||
|
new SeparatorToolItem(),
|
|||
|
new ButtonToolItem(changeProjectSettings),
|
|||
|
new CheckToolItem(record),
|
|||
|
new CheckToolItem(oneX)
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
|
|||
|
Menu = new MenuBar {
|
|||
|
QuitItem=new ButtonMenuItem(close),
|
|||
|
ApplicationItems={
|
|||
|
new ButtonMenuItem(newProject),
|
|||
|
new ButtonMenuItem(openProject),
|
|||
|
new SeparatorMenuItem(),
|
|||
|
new ButtonMenuItem(export),
|
|||
|
new ButtonMenuItem(share),
|
|||
|
|
|||
|
},
|
|||
|
Items={
|
|||
|
|
|||
|
new ButtonMenuItem {
|
|||
|
Text="&Project",
|
|||
|
Items={
|
|||
|
|
|||
|
new CheckMenuItem(record),
|
|||
|
new CheckMenuItem(oneX),
|
|||
|
new ButtonMenuItem(changeProjectSettings)
|
|||
|
}
|
|||
|
},
|
|||
|
new ButtonMenuItem
|
|||
|
{
|
|||
|
Text="&Options",
|
|||
|
Items={
|
|||
|
FsItem,
|
|||
|
ExtSettings
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
};
|
|||
|
}
|
|||
|
|
|||
|
private void NewFrameEvent(object? sender, NewFrameEventArgs e)
|
|||
|
{
|
|||
|
if(synchContext != null)
|
|||
|
synchContext.Post(_ =>
|
|||
|
{
|
|||
|
// HACK: on .NET Core, will be leaked (or delayed GC?)
|
|||
|
// So we could release manually before updates.
|
|||
|
this.d.Image=e.Image;
|
|||
|
d.Image.Dispose();
|
|||
|
}, null);
|
|||
|
}
|
|||
|
|
|||
|
protected override async void OnShown(EventArgs e)
|
|||
|
{ this.synchContext = SynchronizationContext.Current;
|
|||
|
await SetCamera();
|
|||
|
CreateMenu();
|
|||
|
|
|||
|
|
|||
|
|
|||
|
|
|||
|
this.Content=d;
|
|||
|
|
|||
|
|
|||
|
this.SizeChanged+=(sender,e)=>{
|
|||
|
d.Height = ClientSize.Height- d.Location.Y;
|
|||
|
d.Width = ClientSize.Width-d.Location.X;
|
|||
|
};
|
|||
|
}
|
|||
|
public event EventHandler<NewFrameEventArgs>? NewFrame;
|
|||
|
public async Task FrameChanged(Image<Rgb24> image)
|
|||
|
{
|
|||
|
await Instance.SendFrame(image);
|
|||
|
|
|||
|
NewFrame?.Invoke(this,new NewFrameEventArgs(image));
|
|||
|
|
|||
|
|
|||
|
}
|
|||
|
public async Task SetCamera()
|
|||
|
{
|
|||
|
|
|||
|
TimelapseCamera? c=null;
|
|||
|
using(SelectCamera camera=new SelectCamera())
|
|||
|
{
|
|||
|
c=await camera.ShowModalAsync(this);
|
|||
|
}
|
|||
|
svr=new TimelapseWebServer(this);
|
|||
|
Thread t = new Thread(()=>{
|
|||
|
svr.Listen();
|
|||
|
});
|
|||
|
t.Start();
|
|||
|
|
|||
|
if(c == null)
|
|||
|
{
|
|||
|
MessageBox.Show("You will need to restart app to record");
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
////////////////////////////////////////////////
|
|||
|
// Initialize and start capture device
|
|||
|
|
|||
|
// Enumerate capture devices:
|
|||
|
|
|||
|
|
|||
|
// Use first device.
|
|||
|
var descriptor0 =c.DeviceDescriptor;
|
|||
|
if (descriptor0 != null)
|
|||
|
{
|
|||
|
var characteristics = c.VideoCharacteristics;
|
|||
|
// Show status.
|
|||
|
|
|||
|
// Open capture device:
|
|||
|
if(characteristics != null){
|
|||
|
|
|||
|
this.captureDevice = await descriptor0.OpenAsync(
|
|||
|
characteristics,
|
|||
|
this.OnPixelBufferArrived);
|
|||
|
|
|||
|
// Start capturing.
|
|||
|
this.captureDevice.Start();
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
private async void OnPixelBufferArrived(PixelBufferScope bufferScope)
|
|||
|
{
|
|||
|
|
|||
|
////////////////////////////////////////////////
|
|||
|
// Pixel buffer has arrived.
|
|||
|
// NOTE: Perhaps this thread context is NOT UI thread.
|
|||
|
|
|||
|
// Or, refer image data binary directly.
|
|||
|
// (Advanced manipulation, see README.)
|
|||
|
ArraySegment<byte> image = bufferScope.Buffer.ReferImage();
|
|||
|
|
|||
|
// Convert to Stream (using FlashCap.Utilities)
|
|||
|
using (var stream = image.AsStream())
|
|||
|
{
|
|||
|
// Decode image data to a bitmap:
|
|||
|
|
|||
|
Image<Rgb24> img = Image.Load<Rgb24>(stream);
|
|||
|
await FrameChanged(img);
|
|||
|
|
|||
|
// `bitmap` is copied, so we can release pixel buffer now.
|
|||
|
bufferScope.ReleaseNow();
|
|||
|
|
|||
|
// Switch to UI thread.
|
|||
|
// HACK: Here is using `SynchronizationContext.Post()` instead of `Control.Invoke()`.
|
|||
|
// Because in sensitive states when the form is closing,
|
|||
|
// `Control.Invoke()` can fail with exception.
|
|||
|
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
private void Record_CheckedChanged(object? sender,EventArgs args)
|
|||
|
{
|
|||
|
Instance.RecordingChanged -= Recording_Changed;
|
|||
|
Instance.Recording = record.Checked;
|
|||
|
Instance.RecordingChanged += Recording_Changed;
|
|||
|
}
|
|||
|
private void RealTime_CheckedChanged(object? sender,EventArgs args)
|
|||
|
{
|
|||
|
Instance.RealTimeChanged -= RealTime_Changed;
|
|||
|
Instance.RealTime = oneX.Checked;
|
|||
|
Instance.RealTimeChanged += RealTime_Changed;
|
|||
|
}
|
|||
|
|
|||
|
private void Recording_Changed(object? sender,EventArgs args)
|
|||
|
{
|
|||
|
|
|||
|
record.CheckedChanged -= Record_CheckedChanged;
|
|||
|
record.Checked = Instance.Recording;
|
|||
|
record.CheckedChanged += Record_CheckedChanged;
|
|||
|
}
|
|||
|
private void RealTime_Changed(object? sender,EventArgs args)
|
|||
|
{
|
|||
|
oneX.CheckedChanged -= RealTime_CheckedChanged;
|
|||
|
oneX.Checked = Instance.RealTime;
|
|||
|
oneX.CheckedChanged += RealTime_CheckedChanged;
|
|||
|
}
|
|||
|
|
|||
|
public void ProjectClosed(object? sender,EventArgs args)
|
|||
|
{
|
|||
|
|
|||
|
|
|||
|
}
|
|||
|
public void ProjectOpened(object? sender,EventArgs args)
|
|||
|
{
|
|||
|
|
|||
|
if(Instance.Project != null)
|
|||
|
{
|
|||
|
Instance.Recording=false;
|
|||
|
|
|||
|
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
}
|