Refactor: Simplify CameraService, improve session handling, and streamline application initialization

This commit is contained in:
iTob 2026-03-09 22:24:39 +01:00
parent 91935cd41c
commit b3c91da331
5 changed files with 975 additions and 1242 deletions

View File

@ -1,4 +1,3 @@
using System.IO;
using System.Windows;
using CamBooth.App.Core.AppSettings;
@ -21,134 +20,113 @@ namespace CamBooth.App;
/// </summary>
public partial class App : Application
{
private IServiceProvider _serviceProvider;
private IServiceProvider? _serviceProvider;
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
// Konfiguration laden
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production";
var configBuilder = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("Core/AppSettings/app.settings.json", optional: false, reloadOnChange: true);
var configuration = BuildConfiguration();
var services = new ServiceCollection();
if (environment == "Development")
{
configBuilder.AddJsonFile("Core/AppSettings/app.settings.dev.json", optional: true, reloadOnChange: true);
}
RegisterServices(services, configuration);
var configuration = configBuilder.Build();
_serviceProvider = services.BuildServiceProvider();
var services = new ServiceCollection();
services.AddSingleton<IConfiguration>(configuration);
services.AddSingleton<Logger>();
services.AddSingleton<AppSettingsService>();
services.AddSingleton<PictureGalleryService>();
services.AddSingleton<CameraService>();
services.AddSingleton<PhotoPrismAuthService>();
services.AddSingleton<PhotoPrismUploadService>();
services.AddSingleton<PhotoPrismUploadQueueService>();
var tempProvider = services.BuildServiceProvider();
var appSettings = tempProvider.GetRequiredService<AppSettingsService>();
var logger = tempProvider.GetRequiredService<Logger>();
StartBackgroundServices();
try
{
if (!Directory.Exists(appSettings.PictureLocation))
{
Directory.CreateDirectory(appSettings.PictureLocation);
logger.Debug($"Picture directory created: {appSettings.PictureLocation}");
}
}
catch (Exception ex)
{
logger.Error($"Failed to create picture directory: {ex.Message}");
}
// Jetzt die Camera Services basierend auf AppSettings registrieren
// Mit Try-Catch für fehlende DLL-Abhängigkeiten
try
{
if (appSettings.UseMockCamera)
{
services.AddSingleton<ICanonAPI, CanonAPIMock>();
}
else
{
services.AddSingleton<ICanonAPI, CanonAPI>();
}
}
catch (DllNotFoundException ex)
{
// Falls EDSDK DLL nicht gefunden, fallback auf Mock
MessageBox.Show(
$"EDSDK konnte nicht geladen werden. Verwende Mock-Kamera.\n\nFehler: {ex.Message}",
"DLL nicht gefunden",
MessageBoxButton.OK,
MessageBoxImage.Warning);
services.AddSingleton<ICanonAPI, CanonAPIMock>();
}
services.AddTransient<MainWindow>();
_serviceProvider = services.BuildServiceProvider();
// Starte PhotoPrism Upload-Service beim Start
try
{
var uploadQueueService = _serviceProvider.GetRequiredService<PhotoPrismUploadQueueService>();
uploadQueueService.Start();
// Scan für fehlgeschlagene Uploads beim Start
uploadQueueService.ScanAndQueueFailedUploads();
logger.Info("PhotoPrism UploadQueueService initialisiert und gestartet");
}
catch (Exception ex)
{
logger.Error($"Fehler beim Start des PhotoPrism UploadQueueService: {ex.Message}");
}
var mainWindow = _serviceProvider.GetRequiredService<MainWindow>();
mainWindow.Show();
}
protected override void OnExit(ExitEventArgs e)
{
// Stoppe PhotoPrism UploadQueueService beim Beenden der App
try
{
var uploadQueueService = _serviceProvider?.GetService<PhotoPrismUploadQueueService>();
if (uploadQueueService != null)
{
uploadQueueService.StopAsync().Wait(TimeSpan.FromSeconds(10));
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Fehler beim Stoppen des PhotoPrism UploadQueueService: {ex.Message}");
}
// Dispose Service Provider, damit IDisposable-Services (z.B. CameraService) sauber beendet werden.
try
{
if (_serviceProvider is IDisposable disposableProvider)
{
disposableProvider.Dispose();
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Fehler beim Dispose des ServiceProviders: {ex.Message}");
}
base.OnExit(e);
}
_serviceProvider.GetRequiredService<MainWindow>().Show();
}
protected override void OnExit(ExitEventArgs e)
{
try
{
_serviceProvider?.GetService<PhotoPrismUploadQueueService>()
?.StopAsync().Wait(TimeSpan.FromSeconds(10));
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error stopping upload service: {ex.Message}");
}
try
{
(_serviceProvider as IDisposable)?.Dispose();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error disposing service provider: {ex.Message}");
}
base.OnExit(e);
}
private static IConfiguration BuildConfiguration()
{
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production";
var builder = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("Core/AppSettings/app.settings.json", optional: false, reloadOnChange: true);
if (environment == "Development")
builder.AddJsonFile("Core/AppSettings/app.settings.dev.json", optional: true, reloadOnChange: true);
return builder.Build();
}
private static void RegisterServices(ServiceCollection services, IConfiguration configuration)
{
services.AddSingleton(configuration);
services.AddSingleton<Logger>();
services.AddSingleton<AppSettingsService>();
services.AddSingleton<PictureGalleryService>();
services.AddSingleton<CameraService>();
services.AddSingleton<PhotoPrismAuthService>();
services.AddSingleton<PhotoPrismUploadService>();
services.AddSingleton<PhotoPrismUploadQueueService>();
services.AddSingleton<MainWindowViewModel>();
services.AddTransient<MainWindow>();
RegisterCameraApi(services, configuration);
}
private static void RegisterCameraApi(ServiceCollection services, IConfiguration configuration)
{
var useMockCamera = bool.Parse(configuration["AppSettings:UseMockCamera"] ?? "false");
try
{
ICanonAPI canonApi = useMockCamera ? new CanonAPIMock() : new CanonAPI();
services.AddSingleton(canonApi);
}
catch (DllNotFoundException ex)
{
MessageBox.Show(
$"EDSDK konnte nicht geladen werden. Verwende Mock-Kamera.\n\nFehler: {ex.Message}",
"DLL nicht gefunden", MessageBoxButton.OK, MessageBoxImage.Warning);
services.AddSingleton<ICanonAPI>(new CanonAPIMock());
}
}
private void StartBackgroundServices()
{
try
{
var logger = _serviceProvider!.GetRequiredService<Logger>();
var uploadQueueService = _serviceProvider.GetRequiredService<PhotoPrismUploadQueueService>();
uploadQueueService.Start();
uploadQueueService.ScanAndQueueFailedUploads();
logger.Info("PhotoPrism UploadQueueService gestartet");
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error starting background services: {ex.Message}");
}
}
}

View File

@ -14,425 +14,284 @@ namespace CamBooth.App.Features.Camera;
public class CameraService : IDisposable
{
private readonly AppSettingsService _appSettings;
private readonly Logger _logger;
private readonly PictureGalleryService _pictureGalleryService;
private readonly PhotoPrismUploadQueueService _photoPrismUploadQueueService;
private readonly ICanonAPI _APIHandler;
private CameraValue[] AvList;
private int BulbTime = 30;
private List<ICamera> CamList;
private int ErrCount;
private object ErrLock = new();
private bool IsInit;
private CameraValue[] ISOList;
public ICamera? _mainCamera;
private CameraValue[] TvList;
public CameraService(Logger logger,
AppSettingsService appSettings,
PictureGalleryService pictureGalleryService,
PhotoPrismUploadQueueService photoPrismUploadQueueService,
ICanonAPI APIHandler)
{
this._logger = logger;
this._appSettings = appSettings;
this._pictureGalleryService = pictureGalleryService;
this._photoPrismUploadQueueService = photoPrismUploadQueueService;
this._APIHandler = APIHandler;
try
{
this.IsInit = true;
}
catch (DllNotFoundException)
{
this.ReportError("Canon DLLs not found!");
}
catch (Exception ex)
{
this.ReportError(ex.Message);
}
}
/// <summary> Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. </summary>
public void Dispose()
{
this.CloseSession();
this.IsInit = false;
this._APIHandler.Dispose();
this._mainCamera?.Dispose();
}
public void ConnectCamera()
{
ErrorHandler.SevereErrorHappened += this.ErrorHandler_SevereErrorHappened;
ErrorHandler.NonSevereErrorHappened += this.ErrorHandler_NonSevereErrorHappened;
try
{
this.RefreshCamera();
// Retry logic for camera detection (some systems need time to initialize)
int maxRetries = 3;
int retryDelay = 750; // milliseconds
for (int attempt = 0; attempt < maxRetries; attempt++)
{
if (this.CamList != null && this.CamList.Any())
{
break;
}
if (attempt < maxRetries - 1)
{
System.Threading.Thread.Sleep(retryDelay);
this.RefreshCamera();
}
}
if (this.CamList == null || !this.CamList.Any())
{
this.ReportError("No cameras / devices found");
throw new InvalidOperationException("No cameras / devices found after multiple attempts");
}
string cameraDeviceNames = string.Join(", ", this.CamList.Select(cam => cam.DeviceName));
this._logger.Debug(cameraDeviceNames);
// Update _mainCamera reference to the freshly detected camera
this._mainCamera = this.CamList[0];
this._logger.Info($"Selected camera: {this._mainCamera.DeviceName}");
this.OpenSession();
this.SetSettingSaveToComputer();
this.StarLiveView();
}
catch (Exception ex)
{
this._logger.Error($"Error connecting camera: {ex.Message}");
throw;
}
}
private void SetSettingSaveToComputer()
{
this._mainCamera.SetSetting(PropertyID.SaveTo, (int)SaveTo.Host);
this._mainCamera.SetCapacity(4096, int.MaxValue);
}
public void CloseSession()
{
try
{
if (this._mainCamera != null && this._mainCamera.SessionOpen)
{
this._mainCamera.CloseSession();
this._logger.Info("Camera session closed");
}
}
catch (Exception ex)
{
this._logger.Error($"Error closing camera session: {ex.Message}");
}
// AvCoBox.Items.Clear();
// TvCoBox.Items.Clear();
// ISOCoBox.Items.Clear();
// SettingsGroupBox.IsEnabled = false;
// LiveViewGroupBox.IsEnabled = false;
// SessionButton.Content = "Open Session";
// SessionLabel.Content = "No open session";
// StarLVButton.Content = "Start LV";
}
private void RefreshCamera()
{
// CameraListBox.Items.Clear();
this.CamList = this._APIHandler.GetCameraList();
// foreach (Camera cam in CamList) CameraListBox.Items.Add(cam.DeviceName);
// if (_mainCamera?.SessionOpen == true) CameraListBox.SelectedIndex = CamList.FindIndex(t => t.ID == _mainCamera.ID);
// else if (CamList.Count > 0) CameraListBox.SelectedIndex = 0;
}
private void OpenSession()
{
if (this._mainCamera == null)
{
throw new InvalidOperationException("Camera reference is null. Make sure ConnectCamera is called first.");
}
if (this._mainCamera.SessionOpen)
{
this._logger.Info($"Camera session already open for {this._mainCamera.DeviceName}");
return;
}
this._logger.Info($"Opening session for camera: {this._mainCamera.DeviceName}");
const int maxRetries = 3;
const int retryDelayMs = 1000;
for (int attempt = 0; attempt < maxRetries; attempt++)
{
try
{
this._mainCamera.OpenSession();
break;
}
catch (Exception ex) when (attempt < maxRetries - 1 && IsSessionNotOpenError(ex))
{
this._logger.Warning($"OpenSession attempt {attempt + 1}/{maxRetries} failed ({ex.Message}), refreshing camera and retrying in {retryDelayMs}ms...");
System.Threading.Thread.Sleep(retryDelayMs);
this.RefreshCamera();
if (this.CamList?.Any() == true)
{
this._mainCamera = this.CamList[0];
}
}
catch (Exception ex)
{
this._logger.Error($"Failed to open camera session: {ex.Message}");
this.ReportError($"Failed to open camera session: {ex.Message}");
throw;
}
}
this._logger.Info("Camera session opened successfully");
//_mainCamera.ProgressChanged += MainCamera_ProgressChanged;
this._mainCamera.StateChanged += this.MainCamera_StateChanged;
this._mainCamera.DownloadReady += this.MainCamera_DownloadReady;
this.AvList = this._mainCamera.GetSettingsList(PropertyID.Av);
this.TvList = this._mainCamera.GetSettingsList(PropertyID.Tv);
this.ISOList = this._mainCamera.GetSettingsList(PropertyID.ISO);
// ISOCoBox.SelectedIndex = ISOCoBox.Items.IndexOf(ISOValues.GetValue(_mainCamera.GetInt32Setting(PropertyID.ISO)).StringValue);
// SettingsGroupBox.IsEnabled = true;
// LiveViewGroupBox.IsEnabled = true;
}
private void ReportError(string message)
{
this._logger.Info(message);
}
private static bool IsSessionNotOpenError(Exception ex)
{
const string errorName = "SESSION_NOT_OPEN";
return ex.Message.Contains(errorName) || (ex.InnerException?.Message?.Contains(errorName) ?? false);
}
private void StarLiveView()
{
try
{
if (!this._mainCamera.IsLiveViewOn)
{
this._mainCamera.StartLiveView();
}
else
{
this._mainCamera.StopLiveView();
//LVCanvas.Background = Brushes.LightGray;
}
}
catch (Exception ex)
{
this.ReportError(ex.Message);
}
}
public void TakePhoto()
{
try
{
this._mainCamera.TakePhoto();
}
catch (Exception ex)
{
this.ReportError(ex.Message);
throw;
}
}
public async Task PrepareFocusAsync(int focusTimeoutMs = 1500)
{
if (this._mainCamera is not EOSDigital.API.Camera sdkCamera)
{
await Task.Delay(200);
return;
}
var focusCompleted = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
void FocusStateChanged(EOSDigital.API.Camera sender, StateEventID eventId, int parameter)
{
if (eventId == StateEventID.AfResult)
{
focusCompleted.TrySetResult(true);
}
}
sdkCamera.StateChanged += FocusStateChanged;
try
{
await Task.Run(() => sdkCamera.SendCommand(CameraCommand.PressShutterButton, (int)ShutterButton.Halfway));
var completedTask = await Task.WhenAny(focusCompleted.Task, Task.Delay(focusTimeoutMs));
if (completedTask != focusCompleted.Task)
{
this._logger.Info("Autofocus timeout reached, continuing with countdown.");
}
}
catch (Exception ex)
{
this.ReportError(ex.Message);
}
finally
{
try
{
await Task.Run(() => sdkCamera.SendCommand(CameraCommand.PressShutterButton, (int)ShutterButton.OFF));
}
catch (Exception ex)
{
this.ReportError(ex.Message);
}
sdkCamera.StateChanged -= FocusStateChanged;
}
}
#region API Events
// private void APIHandler_CameraAdded(CanonAPI sender)
// {
// try
// {
// }
// catch (Exception ex)
// {
// ReportError(ex.Message, false);
// }
// }
private void MainCamera_StateChanged(EOSDigital.API.Camera sender, StateEventID eventID, int parameter)
{
try
{
if (eventID == StateEventID.Shutdown && this.IsInit)
{
Application.Current.Dispatcher.Invoke(() => this.CloseSession());
//Dispatcher.Invoke((Action)delegate { CloseSession(); });
}
}
catch (Exception ex)
{
this.ReportError(ex.Message);
}
}
// private void MainCamera_ProgressChanged(object sender, int progress)
// {
// try
// {
// //MainProgressBar.Dispatcher.Invoke((Action)delegate { MainProgressBar.Value = progress; });
// }
// catch (Exception ex)
// {
// ReportError(ex.Message, false);
// }
// }
// private void MainCamera_LiveViewUpdated(EOSDigital.API.Camera sender, Stream img)
// {
// try
// {
// using (WrapStream s = new WrapStream(img))
// {
// img.Position = 0;
// BitmapImage EvfImage = new BitmapImage();
// EvfImage.BeginInit();
// EvfImage.StreamSource = s;
// EvfImage.CacheOption = BitmapCacheOption.OnLoad;
// EvfImage.EndInit();
// EvfImage.Freeze();
// Application.Current.Dispatcher.BeginInvoke(SetImageAction, EvfImage);
// }
// }
// catch (Exception ex)
// {
// ReportError(ex.Message, false);
// }
// }
private void MainCamera_DownloadReady(ICamera sender, IDownloadInfo Info)
{
this._logger.Info("MainCamera_DownloadReady called");
try
{
Info.FileName = $"img_{Guid.NewGuid().ToString()}.jpg";
sender.DownloadFile(Info, this._appSettings.PictureLocation);
var savedPhotoPath = Path.Combine(this._appSettings.PictureLocation, Info.FileName);
this._logger.Info("Download complete: " + savedPhotoPath);
Application.Current.Dispatcher.Invoke(() => {
this._pictureGalleryService.IncrementNewPhotoCount();
this._pictureGalleryService.LoadThumbnailsToCache();
});
// Füge neues Foto zur PhotoPrism Upload-Queue hinzu (wenn Auto-Upload aktiviert)
this._photoPrismUploadQueueService.QueueNewPhoto(savedPhotoPath);
this._logger.Info($"Foto zur PhotoPrism Upload-Queue hinzugefügt: {Info.FileName}");
}
catch (Exception ex)
{
this.ReportError(ex.Message);
}
}
private void ErrorHandler_NonSevereErrorHappened(object sender, ErrorCode ex)
{
this.ReportError($"SDK Error code: {ex} ({((int)ex).ToString("X")})");
}
private void ErrorHandler_SevereErrorHappened(object sender, Exception ex)
{
this.ReportError(ex.Message);
}
#endregion
private readonly AppSettingsService _appSettings;
private readonly Logger _logger;
private readonly PictureGalleryService _pictureGalleryService;
private readonly PhotoPrismUploadQueueService _photoPrismUploadQueueService;
private readonly ICanonAPI _canonApi;
private ICamera? _mainCamera;
private List<ICamera>? _camList;
private bool _isConnected;
/// <summary>Fires whenever the camera delivers a new live-view frame.</summary>
public event Action<Stream>? LiveViewUpdated;
public bool IsConnected => _isConnected && _mainCamera?.SessionOpen == true;
public CameraService(
Logger logger,
AppSettingsService appSettings,
PictureGalleryService pictureGalleryService,
PhotoPrismUploadQueueService photoPrismUploadQueueService,
ICanonAPI canonApi)
{
_logger = logger;
_appSettings = appSettings;
_pictureGalleryService = pictureGalleryService;
_photoPrismUploadQueueService = photoPrismUploadQueueService;
_canonApi = canonApi;
}
public void Dispose()
{
CloseSession();
_canonApi.Dispose();
_mainCamera?.Dispose();
}
public void ConnectCamera()
{
ErrorHandler.SevereErrorHappened += ErrorHandler_SevereErrorHappened;
ErrorHandler.NonSevereErrorHappened += ErrorHandler_NonSevereErrorHappened;
try
{
RefreshCameraList();
const int maxRetries = 3;
const int retryDelayMs = 750;
for (int attempt = 0; attempt < maxRetries; attempt++)
{
if (_camList?.Any() == true) break;
if (attempt < maxRetries - 1)
{
System.Threading.Thread.Sleep(retryDelayMs);
RefreshCameraList();
}
}
if (_camList?.Any() != true)
throw new InvalidOperationException("No cameras found after multiple attempts.");
_mainCamera = _camList[0];
_logger.Info($"Camera found: {_mainCamera.DeviceName}");
OpenSession();
SetSaveToComputer();
StartLiveView();
_isConnected = true;
}
catch (Exception ex)
{
_logger.Error($"Error connecting camera: {ex.Message}");
throw;
}
}
public void CloseSession()
{
try
{
if (_mainCamera?.SessionOpen == true)
{
_mainCamera.CloseSession();
_logger.Info("Camera session closed");
}
}
catch (Exception ex)
{
_logger.Error($"Error closing camera session: {ex.Message}");
}
_isConnected = false;
}
public void TakePhoto()
{
if (_mainCamera == null) throw new InvalidOperationException("Camera not connected.");
_mainCamera.TakePhoto();
}
public async Task PrepareFocusAsync(int focusTimeoutMs = 1500)
{
if (_mainCamera is not EOSDigital.API.Camera sdkCamera)
{
await Task.Delay(200);
return;
}
var focusCompleted = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
void FocusStateChanged(EOSDigital.API.Camera sender, StateEventID eventId, int parameter)
{
if (eventId == StateEventID.AfResult)
focusCompleted.TrySetResult(true);
}
sdkCamera.StateChanged += FocusStateChanged;
try
{
await Task.Run(() => sdkCamera.SendCommand(CameraCommand.PressShutterButton, (int)ShutterButton.Halfway));
var completed = await Task.WhenAny(focusCompleted.Task, Task.Delay(focusTimeoutMs));
if (completed != focusCompleted.Task)
_logger.Info("Autofocus timeout reached, continuing.");
}
catch (Exception ex)
{
_logger.Error(ex.Message);
}
finally
{
try
{
await Task.Run(() => sdkCamera.SendCommand(CameraCommand.PressShutterButton, (int)ShutterButton.OFF));
}
catch (Exception ex) { _logger.Error(ex.Message); }
sdkCamera.StateChanged -= FocusStateChanged;
}
}
private void RefreshCameraList() => _camList = _canonApi.GetCameraList();
private void OpenSession()
{
if (_mainCamera == null)
throw new InvalidOperationException("Camera reference is null.");
if (_mainCamera.SessionOpen)
{
_logger.Info($"Session already open for {_mainCamera.DeviceName}");
return;
}
_logger.Info($"Opening session for: {_mainCamera.DeviceName}");
const int maxRetries = 3;
const int retryDelayMs = 1000;
for (int attempt = 0; attempt < maxRetries; attempt++)
{
try
{
_mainCamera.OpenSession();
break;
}
catch (Exception ex) when (attempt < maxRetries - 1 && IsSessionNotOpenError(ex))
{
_logger.Warning($"OpenSession attempt {attempt + 1}/{maxRetries} failed, retrying...");
System.Threading.Thread.Sleep(retryDelayMs);
RefreshCameraList();
if (_camList?.Any() == true)
_mainCamera = _camList[0];
}
catch (Exception ex)
{
_logger.Error($"Failed to open session: {ex.Message}");
throw;
}
}
_logger.Info("Session opened successfully");
_mainCamera.StateChanged += MainCamera_StateChanged;
_mainCamera.DownloadReady += MainCamera_DownloadReady;
_mainCamera.LiveViewUpdated += MainCamera_LiveViewUpdated;
}
private void SetSaveToComputer()
{
_mainCamera!.SetSetting(PropertyID.SaveTo, (int)SaveTo.Host);
_mainCamera.SetCapacity(4096, int.MaxValue);
}
private void StartLiveView()
{
try
{
if (!_mainCamera!.IsLiveViewOn)
_mainCamera.StartLiveView();
else
_mainCamera.StopLiveView();
}
catch (Exception ex)
{
_logger.Error(ex.Message);
}
}
private static bool IsSessionNotOpenError(Exception ex)
{
const string errorName = "SESSION_NOT_OPEN";
return ex.Message.Contains(errorName) || (ex.InnerException?.Message?.Contains(errorName) ?? false);
}
#region Camera event handlers
private void MainCamera_LiveViewUpdated(ICamera sender, Stream img) =>
LiveViewUpdated?.Invoke(img);
private void MainCamera_StateChanged(EOSDigital.API.Camera sender, StateEventID eventID, int parameter)
{
try
{
if (eventID == StateEventID.Shutdown && _isConnected)
Application.Current.Dispatcher.Invoke(CloseSession);
}
catch (Exception ex)
{
_logger.Error(ex.Message);
}
}
private void MainCamera_DownloadReady(ICamera sender, IDownloadInfo info)
{
_logger.Info("Download ready");
try
{
info.FileName = $"img_{Guid.NewGuid()}.jpg";
sender.DownloadFile(info, _appSettings.PictureLocation);
var savedPath = Path.Combine(_appSettings.PictureLocation!, info.FileName);
_logger.Info($"Download complete: {savedPath}");
Application.Current.Dispatcher.Invoke(() =>
{
_pictureGalleryService.IncrementNewPhotoCount();
_pictureGalleryService.LoadThumbnailsToCache();
});
_photoPrismUploadQueueService.QueueNewPhoto(savedPath);
}
catch (Exception ex)
{
_logger.Error(ex.Message);
}
}
private void ErrorHandler_NonSevereErrorHappened(object sender, ErrorCode ex) =>
_logger.Error($"SDK Error: {ex} (0x{(int)ex:X})");
private void ErrorHandler_SevereErrorHappened(object sender, Exception ex) =>
_logger.Error(ex.Message);
#endregion
}

View File

@ -1,10 +1,9 @@
using System.IO;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using CamBooth.App.Core.AppSettings;
using CamBooth.App.Core.Logging;
using CamBooth.App.Features.Camera;
@ -12,87 +11,69 @@ using EOSDigital.API;
namespace CamBooth.App.Features.LiveView;
public partial class LiveViewPage : Page
public partial class LiveViewPage : Page, IDisposable
{
private readonly AppSettingsService _appSettings;
private readonly CameraService _cameraService;
private readonly Logger _logger;
private readonly ImageBrush bgbrush = new();
private readonly Action<BitmapImage> SetImageAction;
private readonly CameraService _cameraService;
private readonly Logger _logger;
private readonly ImageBrush _bgBrush = new();
public LiveViewPage(Logger logger, AppSettingsService appSettings, CameraService cameraService)
{
this._logger = logger;
this._appSettings = appSettings;
this._cameraService = cameraService;
this.InitializeComponent();
this.SetImageAction = img => { this.bgbrush.ImageSource = img; };
// Configure the image brush
this.bgbrush.Stretch = Stretch.UniformToFill;
this.bgbrush.AlignmentX = AlignmentX.Center;
this.bgbrush.AlignmentY = AlignmentY.Center;
this.LVCanvas.Background = this.bgbrush;
// Apply horizontal flip on the Canvas using RenderTransform
TransformGroup transformGroup = new();
transformGroup.Children.Add(new ScaleTransform { ScaleX = -1, ScaleY = 1 });
transformGroup.Children.Add(new TranslateTransform { X = 1, Y = 0 });
this.LVCanvas.RenderTransform = transformGroup;
this.LVCanvas.RenderTransformOrigin = new Point(0.5, 0.5);
try
{
cameraService.ConnectCamera();
// Verify that camera session is open before subscribing to events
if (cameraService._mainCamera != null && cameraService._mainCamera.SessionOpen)
{
cameraService._mainCamera.LiveViewUpdated += this.MainCamera_OnLiveViewUpdated;
this._logger.Info("LiveViewPage initialized successfully");
}
else
{
this._logger.Error("Camera session is not open after connection attempt");
throw new InvalidOperationException("Camera session failed to open");
}
}
catch (Exception ex)
{
this._logger.Error($"Failed to initialize LiveViewPage: {ex.Message}");
throw;
}
}
public LiveViewPage(Logger logger, CameraService cameraService)
{
_logger = logger;
_cameraService = cameraService;
InitializeComponent();
_bgBrush.Stretch = Stretch.UniformToFill;
_bgBrush.AlignmentX = AlignmentX.Center;
_bgBrush.AlignmentY = AlignmentY.Center;
LVCanvas.Background = _bgBrush;
var transformGroup = new TransformGroup();
transformGroup.Children.Add(new ScaleTransform { ScaleX = -1, ScaleY = 1 });
transformGroup.Children.Add(new TranslateTransform { X = 1, Y = 0 });
LVCanvas.RenderTransform = transformGroup;
LVCanvas.RenderTransformOrigin = new Point(0.5, 0.5);
try
{
cameraService.ConnectCamera();
cameraService.LiveViewUpdated += OnLiveViewUpdated;
_logger.Info("LiveViewPage initialized successfully");
}
catch (Exception ex)
{
_logger.Error($"Failed to initialize LiveViewPage: {ex.Message}");
throw;
}
}
private void MainCamera_OnLiveViewUpdated(ICamera sender, Stream img)
{
try
{
using WrapStream s = new(img);
img.Position = 0;
BitmapImage EvfImage = new();
EvfImage.BeginInit();
EvfImage.StreamSource = s;
EvfImage.CacheOption = BitmapCacheOption.OnLoad;
EvfImage.EndInit();
EvfImage.Freeze();
Application.Current.Dispatcher.BeginInvoke(this.SetImageAction, EvfImage);
}
catch (Exception ex)
{
this._logger.Error(ex.Message);
}
}
public void Dispose()
{
this._cameraService.Dispose();
}
}
public void Dispose()
{
_cameraService.LiveViewUpdated -= OnLiveViewUpdated;
_cameraService.Dispose();
}
private void OnLiveViewUpdated(Stream img)
{
try
{
using WrapStream s = new(img);
img.Position = 0;
var evfImage = new BitmapImage();
evfImage.BeginInit();
evfImage.StreamSource = s;
evfImage.CacheOption = BitmapCacheOption.OnLoad;
evfImage.EndInit();
evfImage.Freeze();
Application.Current.Dispatcher.BeginInvoke(() => _bgBrush.ImageSource = evfImage);
}
catch (Exception ex)
{
_logger.Error(ex.Message);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,148 @@
using System.IO;
using System.Threading.Tasks;
using System.Windows.Threading;
using CamBooth.App.Core.AppSettings;
using CamBooth.App.Core.Logging;
using CamBooth.App.Features.Camera;
using CamBooth.App.Features.PhotoPrismUpload;
using CamBooth.App.Features.PictureGallery;
namespace CamBooth.App;
public class MainWindowViewModel
{
private readonly Logger _logger;
private readonly AppSettingsService _appSettings;
private readonly PictureGalleryService _pictureGalleryService;
private readonly CameraService _cameraService;
private readonly PhotoPrismUploadService _photoPrismUploadService;
private readonly DispatcherTimer _galleryPromptTimer;
public bool IsPhotoProcessRunning { get; private set; }
// Events → MainWindow subscribes and updates UI
public event Action<string, string>? LoadingProgressChanged; // (statusText, countText)
public event Action? InitializationCompleted;
public event Action? GalleryPromptRequested;
public event Action? GalleryPromptDismissed;
public MainWindowViewModel(
Logger logger,
AppSettingsService appSettings,
PictureGalleryService pictureGalleryService,
CameraService cameraService,
PhotoPrismUploadService photoPrismUploadService)
{
_logger = logger;
_appSettings = appSettings;
_pictureGalleryService = pictureGalleryService;
_cameraService = cameraService;
_photoPrismUploadService = photoPrismUploadService;
_galleryPromptTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(5) };
_galleryPromptTimer.Tick += (_, _) => DismissGalleryPrompt();
if (appSettings.PhotoPrismAutoUploadEnabled)
_ = InitializePhotoUploadAsync();
}
public async Task InitializeAsync()
{
try
{
await LoadThumbnailsAsync();
await Task.Delay(500);
}
catch (Exception ex)
{
_logger.Error($"Initialization error: {ex.Message}");
}
finally
{
InitializationCompleted?.Invoke();
}
}
/// <summary>
/// Called by MainWindow when the countdown timer fires and a photo should be taken.
/// Throws on camera error so MainWindow can show a message box.
/// </summary>
public void OnTimerElapsed()
{
IsPhotoProcessRunning = false;
_cameraService.TakePhoto();
ShowGalleryPrompt();
}
public void StartPhotoProcess()
{
IsPhotoProcessRunning = true;
DismissGalleryPrompt();
}
public void CancelPhotoProcess()
{
IsPhotoProcessRunning = false;
}
public void ShowGalleryPrompt()
{
GalleryPromptRequested?.Invoke();
_galleryPromptTimer.Stop();
_galleryPromptTimer.Start();
}
public void DismissGalleryPrompt()
{
_galleryPromptTimer.Stop();
GalleryPromptDismissed?.Invoke();
}
private async Task LoadThumbnailsAsync()
{
var pictureLocation = _appSettings.PictureLocation;
if (!Directory.Exists(pictureLocation))
{
LoadingProgressChanged?.Invoke("Keine Fotos gefunden", "0 Fotos");
await Task.Delay(1000);
return;
}
string[] imageExtensions = [".jpg", ".jpeg", ".png", ".bmp", ".gif"];
var totalCount = Directory.EnumerateFiles(pictureLocation!)
.Count(f => imageExtensions.Contains(Path.GetExtension(f).ToLowerInvariant()));
if (totalCount == 0)
{
LoadingProgressChanged?.Invoke("Keine Fotos gefunden", "Bereit für neue Aufnahmen!");
await Task.Delay(1000);
return;
}
var photoLabel = totalCount == 1 ? "Foto" : "Fotos";
LoadingProgressChanged?.Invoke($"Lade {totalCount} {photoLabel}...", $"0 / {totalCount}");
await _pictureGalleryService.LoadThumbnailsToCache();
LoadingProgressChanged?.Invoke("Fotos erfolgreich geladen!", $"{totalCount} {photoLabel} bereit");
}
private async Task InitializePhotoUploadAsync()
{
_logger.Info("PhotoPrism Auto-Upload aktiviert. Authentifiziere...");
var authSuccess = await _photoPrismUploadService.AuthenticateAsync();
if (authSuccess)
_logger.Info("PhotoPrism-Authentifizierung erfolgreich!");
else
_logger.Warning("PhotoPrism-Authentifizierung fehlgeschlagen.");
}
}