|
@@ -46,39 +46,36 @@ export function CameraPage() {
|
|
|
}, [printer]);
|
|
}, [printer]);
|
|
|
|
|
|
|
|
// Cleanup on unmount - stop the camera stream
|
|
// Cleanup on unmount - stop the camera stream
|
|
|
|
|
+ // Track if we've already sent the stop signal to avoid duplicate calls
|
|
|
|
|
+ const stopSentRef = useRef(false);
|
|
|
|
|
+
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
const stopUrl = `/api/v1/printers/${id}/camera/stop`;
|
|
const stopUrl = `/api/v1/printers/${id}/camera/stop`;
|
|
|
|
|
+ stopSentRef.current = false;
|
|
|
|
|
|
|
|
- // Handle page unload/close with sendBeacon (more reliable than fetch on unload)
|
|
|
|
|
- const handleBeforeUnload = () => {
|
|
|
|
|
- if (id > 0) {
|
|
|
|
|
|
|
+ const sendStopOnce = () => {
|
|
|
|
|
+ if (id > 0 && !stopSentRef.current) {
|
|
|
|
|
+ stopSentRef.current = true;
|
|
|
navigator.sendBeacon(stopUrl);
|
|
navigator.sendBeacon(stopUrl);
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- // Handle visibility change (tab hidden/closed)
|
|
|
|
|
- const handleVisibilityChange = () => {
|
|
|
|
|
- if (document.visibilityState === 'hidden' && id > 0) {
|
|
|
|
|
- navigator.sendBeacon(stopUrl);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // Handle page unload/close with sendBeacon (more reliable than fetch on unload)
|
|
|
|
|
+ const handleBeforeUnload = () => {
|
|
|
|
|
+ sendStopOnce();
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
window.addEventListener('beforeunload', handleBeforeUnload);
|
|
window.addEventListener('beforeunload', handleBeforeUnload);
|
|
|
- document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
return () => {
|
|
|
window.removeEventListener('beforeunload', handleBeforeUnload);
|
|
window.removeEventListener('beforeunload', handleBeforeUnload);
|
|
|
- document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
|
|
|
|
|
|
|
|
- // Clear the image source
|
|
|
|
|
|
|
+ // Clear the image source first to stop the stream
|
|
|
if (imgRef.current) {
|
|
if (imgRef.current) {
|
|
|
imgRef.current.src = '';
|
|
imgRef.current.src = '';
|
|
|
}
|
|
}
|
|
|
- // Call the stop endpoint to terminate ffmpeg processes
|
|
|
|
|
- if (id > 0) {
|
|
|
|
|
- // Use sendBeacon for reliability during unmount
|
|
|
|
|
- navigator.sendBeacon(stopUrl);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // Send stop signal only once
|
|
|
|
|
+ sendStopOnce();
|
|
|
};
|
|
};
|
|
|
}, [id]);
|
|
}, [id]);
|
|
|
|
|
|
|
@@ -93,19 +90,31 @@ export function CameraPage() {
|
|
|
}
|
|
}
|
|
|
}, [streamMode, streamLoading, imageKey, transitioning]);
|
|
}, [streamMode, streamLoading, imageKey, transitioning]);
|
|
|
|
|
|
|
|
- // Fullscreen change listener
|
|
|
|
|
|
|
+ // Fullscreen change listener - refresh stream after fullscreen transition
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
const handleFullscreenChange = () => {
|
|
const handleFullscreenChange = () => {
|
|
|
- setIsFullscreen(!!document.fullscreenElement);
|
|
|
|
|
|
|
+ const nowFullscreen = !!document.fullscreenElement;
|
|
|
|
|
+ setIsFullscreen(nowFullscreen);
|
|
|
|
|
+
|
|
|
|
|
+ // Refresh stream after fullscreen transition to prevent stall
|
|
|
|
|
+ if (streamMode === 'stream' && !transitioning) {
|
|
|
|
|
+ // Clear image src first, then set new key after delay
|
|
|
|
|
+ if (imgRef.current) {
|
|
|
|
|
+ imgRef.current.src = '';
|
|
|
|
|
+ }
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ setStreamLoading(true);
|
|
|
|
|
+ setImageKey(Date.now());
|
|
|
|
|
+ }, 200);
|
|
|
|
|
+ }
|
|
|
};
|
|
};
|
|
|
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
|
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
|
|
return () => document.removeEventListener('fullscreenchange', handleFullscreenChange);
|
|
return () => document.removeEventListener('fullscreenchange', handleFullscreenChange);
|
|
|
- }, []);
|
|
|
|
|
|
|
+ }, [streamMode, transitioning]);
|
|
|
|
|
|
|
|
- // Save window size and position when user resizes or moves (only for popup windows)
|
|
|
|
|
|
|
+ // Save window size and position when user resizes or moves
|
|
|
|
|
+ // Works for both popup windows and standalone camera pages
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
- if (!window.opener) return;
|
|
|
|
|
-
|
|
|
|
|
let saveTimeout: NodeJS.Timeout;
|
|
let saveTimeout: NodeJS.Timeout;
|
|
|
const saveWindowState = () => {
|
|
const saveWindowState = () => {
|
|
|
// Debounce to avoid saving during drag
|
|
// Debounce to avoid saving during drag
|
|
@@ -121,20 +130,9 @@ export function CameraPage() {
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
window.addEventListener('resize', saveWindowState);
|
|
window.addEventListener('resize', saveWindowState);
|
|
|
- // Use interval to detect position changes (no native 'move' event)
|
|
|
|
|
- const positionInterval = setInterval(() => {
|
|
|
|
|
- const saved = localStorage.getItem('cameraWindowState');
|
|
|
|
|
- if (saved) {
|
|
|
|
|
- const state = JSON.parse(saved);
|
|
|
|
|
- if (state.left !== window.screenX || state.top !== window.screenY) {
|
|
|
|
|
- saveWindowState();
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }, 1000);
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
return () => {
|
|
|
clearTimeout(saveTimeout);
|
|
clearTimeout(saveTimeout);
|
|
|
- clearInterval(positionInterval);
|
|
|
|
|
window.removeEventListener('resize', saveWindowState);
|
|
window.removeEventListener('resize', saveWindowState);
|
|
|
};
|
|
};
|
|
|
}, []);
|
|
}, []);
|
|
@@ -263,8 +261,8 @@ export function CameraPage() {
|
|
|
clearInterval(countdownIntervalRef.current);
|
|
clearInterval(countdownIntervalRef.current);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Auto-resize popup window to fit video content (only if no saved preference)
|
|
|
|
|
- if (window.opener && imgRef.current && !localStorage.getItem('cameraWindowState')) {
|
|
|
|
|
|
|
+ // Auto-resize window to fit video content (only if no saved preference)
|
|
|
|
|
+ if (imgRef.current && !localStorage.getItem('cameraWindowState')) {
|
|
|
const img = imgRef.current;
|
|
const img = imgRef.current;
|
|
|
const videoWidth = img.naturalWidth;
|
|
const videoWidth = img.naturalWidth;
|
|
|
const videoHeight = img.naturalHeight;
|
|
const videoHeight = img.naturalHeight;
|
|
@@ -281,7 +279,11 @@ export function CameraPage() {
|
|
|
const targetWidth = videoWidth + padding + chromeWidth;
|
|
const targetWidth = videoWidth + padding + chromeWidth;
|
|
|
const targetHeight = videoHeight + headerHeight + padding + chromeHeight;
|
|
const targetHeight = videoHeight + headerHeight + padding + chromeHeight;
|
|
|
|
|
|
|
|
- window.resizeTo(targetWidth, targetHeight);
|
|
|
|
|
|
|
+ try {
|
|
|
|
|
+ window.resizeTo(targetWidth, targetHeight);
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ // resizeTo may not be allowed in all contexts
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|