diff --git a/src/CamBooth/CamBooth.App/App.xaml.cs b/src/CamBooth/CamBooth.App/App.xaml.cs index efb837f..cc61ae6 100644 --- a/src/CamBooth/CamBooth.App/App.xaml.cs +++ b/src/CamBooth/CamBooth.App/App.xaml.cs @@ -38,6 +38,12 @@ StartBackgroundServices(); _serviceProvider.GetRequiredService().Show(); } +protected override void OnSessionEnding(SessionEndingCancelEventArgs e) +{ +TryShutdownCamera("OnSessionEnding"); +base.OnSessionEnding(e); +} + protected override void OnExit(ExitEventArgs e) { try @@ -50,6 +56,8 @@ catch (Exception ex) System.Diagnostics.Debug.WriteLine($"Error stopping upload service: {ex.Message}"); } +TryShutdownCamera("OnExit"); + try { (_serviceProvider as IDisposable)?.Dispose(); @@ -129,4 +137,17 @@ catch (Exception ex) System.Diagnostics.Debug.WriteLine($"Error starting background services: {ex.Message}"); } } + +private void TryShutdownCamera(string source) +{ +try +{ +var cameraService = _serviceProvider?.GetService(); +cameraService?.CloseSession(); +} +catch (Exception ex) +{ +System.Diagnostics.Debug.WriteLine($"Error closing camera session ({source}): {ex.Message}"); +} +} } diff --git a/src/CamBooth/CamBooth.App/Features/Camera/CameraService.cs b/src/CamBooth/CamBooth.App/Features/Camera/CameraService.cs index 8b6369a..bc16049 100644 --- a/src/CamBooth/CamBooth.App/Features/Camera/CameraService.cs +++ b/src/CamBooth/CamBooth.App/Features/Camera/CameraService.cs @@ -1,4 +1,5 @@ using System.IO; +using Timer = System.Timers.Timer; using System.Threading.Tasks; using System.Windows; using CamBooth.App.Core.AppSettings; @@ -21,6 +22,7 @@ public class CameraService : IDisposable private ICamera? _mainCamera; private List? _camList; private bool _isConnected; + private Timer? _shutdownKeepAliveTimer; /// Fires whenever the camera delivers a new live-view frame. public event Action? LiveViewUpdated; @@ -99,6 +101,7 @@ public class CameraService : IDisposable { if (_mainCamera?.SessionOpen == true) { + StopShutdownKeepAlive(); _mainCamera.CloseSession(); _logger.Info("Camera session closed"); } @@ -209,6 +212,9 @@ public class CameraService : IDisposable _mainCamera.StateChanged += MainCamera_StateChanged; _mainCamera.DownloadReady += MainCamera_DownloadReady; _mainCamera.LiveViewUpdated += MainCamera_LiveViewUpdated; + _mainCamera.LiveViewStopped += MainCamera_LiveViewStopped; + _mainCamera.CameraHasShutdown += MainCamera_CameraHasShutdown; + StartShutdownKeepAlive(); } @@ -225,8 +231,6 @@ public class CameraService : IDisposable { if (!_mainCamera!.IsLiveViewOn) _mainCamera.StartLiveView(); - else - _mainCamera.StopLiveView(); } catch (Exception ex) { @@ -247,11 +251,29 @@ public class CameraService : IDisposable private void MainCamera_LiveViewUpdated(ICamera sender, Stream img) => LiveViewUpdated?.Invoke(img); + private void MainCamera_LiveViewStopped(ICamera sender) + { + if (!_isConnected) return; + + _logger.Warning("LiveView stopped; attempting restart."); + try + { + sender.StartLiveView(); + } + catch (Exception ex) + { + _logger.Error($"Failed to restart LiveView: {ex.Message}"); + } + } + private void MainCamera_StateChanged(EOSDigital.API.Camera sender, StateEventID eventID, int parameter) { try { + if (eventID == StateEventID.WillSoonShutDown || eventID == StateEventID.ShutDownTimerUpdate) + ExtendShutdownTimer(); + if (eventID == StateEventID.Shutdown && _isConnected) Application.Current.Dispatcher.Invoke(CloseSession); } @@ -294,5 +316,49 @@ public class CameraService : IDisposable private void ErrorHandler_SevereErrorHappened(object sender, Exception ex) => _logger.Error(ex.Message); + private void MainCamera_CameraHasShutdown(ICamera sender) + { + try + { + StopShutdownKeepAlive(); + } + catch (Exception ex) + { + _logger.Error($"Failed stopping shutdown keep-alive: {ex.Message}"); + } + } + #endregion -} \ No newline at end of file + + private void StartShutdownKeepAlive() + { + if (_shutdownKeepAliveTimer != null) return; + + _shutdownKeepAliveTimer = new Timer(20_000); + _shutdownKeepAliveTimer.AutoReset = true; + _shutdownKeepAliveTimer.Elapsed += (_, _) => ExtendShutdownTimer(); + _shutdownKeepAliveTimer.Start(); + } + + private void StopShutdownKeepAlive() + { + if (_shutdownKeepAliveTimer == null) return; + + _shutdownKeepAliveTimer.Stop(); + _shutdownKeepAliveTimer.Dispose(); + _shutdownKeepAliveTimer = null; + } + + private void ExtendShutdownTimer() + { + if (_mainCamera == null || !_mainCamera.SessionOpen) return; + try + { + _mainCamera.SendCommand(CameraCommand.ExtendShutDownTimer); + } + catch (Exception ex) + { + _logger.Warning($"ExtendShutDownTimer failed: {ex.Message}"); + } + } +} diff --git a/src/CamBooth/CamBooth.App/Features/LiveView/LiveViewPage.xaml.cs b/src/CamBooth/CamBooth.App/Features/LiveView/LiveViewPage.xaml.cs index c677e17..fa6582d 100644 --- a/src/CamBooth/CamBooth.App/Features/LiveView/LiveViewPage.xaml.cs +++ b/src/CamBooth/CamBooth.App/Features/LiveView/LiveViewPage.xaml.cs @@ -1,79 +1,166 @@ +using System; +using System.Diagnostics; using System.IO; +using System.Threading; +using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Media.Imaging; - +using System.Windows.Threading; using CamBooth.App.Core.Logging; using CamBooth.App.Features.Camera; - using EOSDigital.API; namespace CamBooth.App.Features.LiveView; public partial class LiveViewPage : Page, IDisposable { -private readonly CameraService _cameraService; -private readonly Logger _logger; -private readonly ImageBrush _bgBrush = new(); + private readonly CameraService _cameraService; + private readonly Logger _logger; + private readonly ImageBrush _bgBrush = new(); + private int _targetPixelWidth; + private readonly object _frameLock = new(); + private byte[]? _latestFrame; + private int _workerRunning; -public LiveViewPage(Logger logger, CameraService cameraService) -{ -_logger = logger; -_cameraService = cameraService; + public LiveViewPage(Logger logger, CameraService cameraService) + { + _logger = logger; + _cameraService = cameraService; -InitializeComponent(); + InitializeComponent(); -_bgBrush.Stretch = Stretch.UniformToFill; -_bgBrush.AlignmentX = AlignmentX.Center; -_bgBrush.AlignmentY = AlignmentY.Center; -LVCanvas.Background = _bgBrush; + _bgBrush.Stretch = Stretch.UniformToFill; + _bgBrush.AlignmentX = AlignmentX.Center; + _bgBrush.AlignmentY = AlignmentY.Center; + LVCanvas.Background = _bgBrush; + if (LVCanvas.ActualWidth > 0) + _targetPixelWidth = (int)Math.Round(LVCanvas.ActualWidth); + RenderOptions.SetBitmapScalingMode(LVCanvas, BitmapScalingMode.LowQuality); -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); + 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); + LVCanvas.SizeChanged += (_, e) => + { + var width = (int)Math.Round(e.NewSize.Width); + if (width > 0) _targetPixelWidth = width; + }; -try -{ -cameraService.ConnectCamera(); -cameraService.LiveViewUpdated += OnLiveViewUpdated; -_logger.Info("LiveViewPage initialized successfully"); -} -catch (Exception ex) -{ -_logger.Error($"Failed to initialize LiveViewPage: {ex.Message}"); -throw; -} -} - - -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); -} -} + try + { + cameraService.ConnectCamera(); + cameraService.LiveViewUpdated += OnLiveViewUpdated; + _logger.Info("LiveViewPage initialized successfully"); + } + catch (Exception ex) + { + _logger.Error($"Failed to initialize LiveViewPage: {ex.Message}"); + throw; + } + } + + + public void Dispose() + { + _cameraService.LiveViewUpdated -= OnLiveViewUpdated; + _cameraService.Dispose(); + } + + + private void OnLiveViewUpdated(Stream img) + { + if (img == null) return; + if (!img.CanRead) return; + + try + { + if (img.CanSeek) img.Position = 0; + using var ms = new MemoryStream(); + img.CopyTo(ms); + lock (_frameLock) + { + _latestFrame = ms.ToArray(); + } + TryStartWorker(); + } + catch (Exception ex) + { + _logger.Error($"LiveView update failed: {ex}"); + } + } + + private void TryStartWorker() + { + if (Interlocked.Exchange(ref _workerRunning, 1) == 1) + return; + + _ = Task.Run(ProcessFramesAsync); + } + + private async Task ProcessFramesAsync() + { + const int targetFps = 60; + const double minFrameMs = 1000d / targetFps; + + try + { + while (true) + { + var sw = Stopwatch.StartNew(); + byte[]? frame; + lock (_frameLock) + { + frame = _latestFrame; + _latestFrame = null; + } + + if (frame == null) + break; + + BitmapImage? evfImage = null; + try + { + using var ms = new MemoryStream(frame); + evfImage = new BitmapImage(); + evfImage.BeginInit(); + evfImage.StreamSource = ms; + evfImage.CacheOption = BitmapCacheOption.OnLoad; + if (_targetPixelWidth > 0) + evfImage.DecodePixelWidth = _targetPixelWidth; + evfImage.EndInit(); + evfImage.Freeze(); + } + catch (Exception ex) + { + _logger.Error($"LiveView decode failed: {ex}"); + } + + if (evfImage != null) + { + await Application.Current.Dispatcher.InvokeAsync( + () => _bgBrush.ImageSource = evfImage, + DispatcherPriority.Render); + } + + sw.Stop(); + var remainingMs = minFrameMs - sw.Elapsed.TotalMilliseconds; + if (remainingMs > 0) + await Task.Delay((int)remainingMs); + } + } + finally + { + Interlocked.Exchange(ref _workerRunning, 0); + lock (_frameLock) + { + if (_latestFrame != null) + TryStartWorker(); + } + } + } } diff --git a/src/CamBooth/CamBooth.App/Features/PictureGallery/PictureGalleryPage.xaml.cs b/src/CamBooth/CamBooth.App/Features/PictureGallery/PictureGalleryPage.xaml.cs index 9df0c98..b81875c 100644 --- a/src/CamBooth/CamBooth.App/Features/PictureGallery/PictureGalleryPage.xaml.cs +++ b/src/CamBooth/CamBooth.App/Features/PictureGallery/PictureGalleryPage.xaml.cs @@ -160,7 +160,7 @@ public partial class PictureGalleryPage : Page public async Task ShowPhotoDialogAsync(string picturePath) { - var imageToShow = new System.Windows.Controls.Image + var imageToShow = new Image { MaxHeight = 570, MaxWidth = 800, diff --git a/src/CamBooth/CamBooth.App/rechtliches.html b/src/CamBooth/CamBooth.App/rechtliches.html new file mode 100644 index 0000000..3f5c1e7 --- /dev/null +++ b/src/CamBooth/CamBooth.App/rechtliches.html @@ -0,0 +1,193 @@ + + + + + + + Rechtliches – Impressum & Datenschutz + + + +
+
+

Rechtliches

+

Impressum & Datenschutzerklärung für die private Fotobox‑Online‑Galerie

+
+ +
+

Impressum Deutschland

+
+
+
Betreiber
+
Tobias Wohlleben
+
+
+
Anschrift
+
Grethener Str. 32, 04668 Parthenstein
+
+
+
E‑Mail
+
info@fgrimma-fotobox.de
+
+
+ +

Datenschutzerklärung

+ +

1. Verantwortlicher

+

Verantwortlich für die Verarbeitung personenbezogener Daten im Sinne der Datenschutz‑Grundverordnung (DSGVO) ist die im Impressum genannte Person.

+ +

2. Zweck der Verarbeitung

+

Diese Webseite dient der Bereitstellung einer privaten Online‑Galerie (PhotoPrism). Nutzer/Teilnehmer einer Veranstaltung können die mit der Fotobox aufgenommenen Fotos ansehen und herunterladen. Der Zugriff erfolgt über individuelle Album‑Share‑Links (z. B. per QR‑Code).

+ +

3. Art der verarbeiteten Daten

+
    +
  • Bilddaten (Fotos), auf denen Personen erkennbar sein können.
  • +
  • Technische Zugriffsdaten (Server‑Logfiles), z. B. IP‑Adresse, Datum/Uhrzeit, aufgerufene Ressourcen, User‑Agent.
  • +
  • Nutzungsdaten (aggregierte Statistikdaten) im Rahmen der Webanalyse mit Umami (self‑hosted).
  • +
+ +

4. Rechtsgrundlagen

+
    +
  • Bereitstellung der Galerie / Verarbeitung der Fotos: Einwilligung (Art. 6 Abs. 1 lit. a DSGVO). Die erforderlichen Informationen werden zusätzlich direkt an der Fotobox bereitgestellt.
  • +
  • Server‑Logfiles: Berechtigtes Interesse (Art. 6 Abs. 1 lit. f DSGVO) an IT‑Sicherheit, Stabilität, Fehleranalyse und Missbrauchsabwehr.
  • +
  • Webanalyse (Umami): Berechtigtes Interesse (Art. 6 Abs. 1 lit. f DSGVO) an datensparsamer Reichweitenmessung und technischer Optimierung. Umami wird ohne Cookies betrieben.
  • +
+ +

5. Zugriff auf die Alben / Empfängerkreis

+

Die Fotoalben sind nicht öffentlich indexierbar. Zugriff erhalten nur Personen, die den jeweiligen Album‑Share‑Link/QR‑Code besitzen (ggf. zusätzlich passwortgeschützt). Eine Veröffentlichung der Fotos außerhalb der Galerie erfolgt nicht durch den Betreiber.

+ +

6. Speicherdauer / Löschung

+

Die Fotos werden für 6 Monate ab dem Folgetag nach dem Event vorgehalten und anschließend automatisch gelöscht, sofern keine Gründe für eine längere Speicherung bestehen (z. B. zur Abwehr von Rechtsansprüchen im Einzelfall).

+

Server‑Logfiles werden nur so lange gespeichert, wie es für den technischen Betrieb und die Sicherheit erforderlich ist (in der Regel kurzzeitig) und anschließend gelöscht bzw. rotiert.

+ +

7. Hosting / Auftragsverarbeitung

+

Die Systeme werden auf einem virtuellen Server (VPS) bei STRATO in Deutschland betrieben. Der Hosting‑Anbieter verarbeitet dabei technische Daten (z. B. IP‑Adressen in Server‑Logs) im Rahmen der Bereitstellung der Infrastruktur.

+ +

8. Webanalyse mit Umami (self‑hosted)

+

Zur statistischen Auswertung der Nutzung setzen wir Umami ein (self‑hosted). Umami wird ohne Cookies betrieben. Es werden keine Profile über einzelne Besucher erstellt. Die Auswertung erfolgt in aggregierter Form (z. B. Seitenaufrufe). Da Umami auf dem eigenen Server betrieben wird, werden keine Analysedaten an einen Drittanbieter übermittelt.

+ +

9. Sicherheit (HTTPS)

+

Die Verbindung zu dieser Webseite erfolgt verschlüsselt (HTTPS), um Inhalte und Zugangsdaten während der Übertragung zu schützen.

+ +

10. Rechte der betroffenen Personen

+

Du hast nach der DSGVO insbesondere folgende Rechte: Auskunft (Art. 15), Berichtigung (Art. 16), Löschung (Art. 17), Einschränkung der Verarbeitung (Art. 18), Datenübertragbarkeit (Art. 20, soweit anwendbar), Widerspruch (Art. 21) sowie Widerruf erteilter Einwilligungen mit Wirkung für die Zukunft (Art. 7 Abs. 3).

+

Wenn du die Entfernung eines Fotos wünschst, schreibe bitte an info@grimma-fotobox.de und nenne (wenn möglich) das Event/Album und das betroffene Foto.

+ +

11. Beschwerderecht

+

Du hast das Recht, dich bei einer Datenschutzaufsichtsbehörde zu beschweren (Art. 77 DSGVO).

+ +

12. Stand

+

Stand: 10.03.2026

+ +
+
+ +