From ff95c001f42b8039e54484d09e5d4e7b07e03145 Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Thu, 24 Jul 2025 18:59:20 +0800 Subject: [PATCH] Fix: image retry in mobile app displays error --- src/components/ImageMethodDialog.vue | 89 +++++++++++++++++++++++++--- 1 file changed, 81 insertions(+), 8 deletions(-) diff --git a/src/components/ImageMethodDialog.vue b/src/components/ImageMethodDialog.vue index 7564e913..b6cdd9bd 100644 --- a/src/components/ImageMethodDialog.vue +++ b/src/components/ImageMethodDialog.vue @@ -20,7 +20,7 @@
Camera preview not started. @@ -328,6 +328,9 @@ export default class ImageMethodDialog extends Vue { /** Whether to show camera preview */ showCameraPreview = false; + /** Whether currently retrying camera preview */ + isRetrying = false; + /** Camera stream reference */ private cameraStream: MediaStream | null = null; @@ -589,6 +592,7 @@ export default class ImageMethodDialog extends Vue { } this.blob = undefined; this.showCameraPreview = false; + this.isRetrying = false; } async startCameraPreview() { @@ -600,6 +604,13 @@ export default class ImageMethodDialog extends Vue { "getUserMedia available:", !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia), ); + logger.debug("Current facing mode:", this.currentFacingMode); + + // If camera is already active, don't restart it + if (this.cameraState === "active" && this.cameraStream) { + logger.debug("Camera is already active, skipping restart"); + return; + } try { this.cameraState = "initializing"; @@ -611,10 +622,17 @@ export default class ImageMethodDialog extends Vue { throw new Error("Camera API not available in this browser"); } + logger.debug( + "Requesting camera stream with facingMode:", + this.currentFacingMode, + ); const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: this.currentFacingMode }, }); - logger.debug("Camera access granted"); + logger.debug( + "Camera access granted, stream tracks:", + stream.getTracks().map((t) => ({ kind: t.kind, label: t.label })), + ); this.cameraStream = stream; this.cameraState = "active"; this.cameraStateMessage = "Camera is active"; @@ -623,20 +641,26 @@ export default class ImageMethodDialog extends Vue { const videoElement = this.$refs.videoElement as HTMLVideoElement; if (videoElement) { + logger.debug("Setting video element srcObject"); videoElement.srcObject = stream; - await new Promise((resolve) => { + await new Promise((resolve, reject) => { videoElement.onloadedmetadata = () => { + logger.debug("Video metadata loaded, starting playback"); videoElement .play() .then(() => { - logger.debug("Video element started playing"); + logger.debug("Video element started playing successfully"); resolve(true); }) .catch((error) => { logger.error("Error playing video:", error); - throw error; + reject(error); }); }; + videoElement.onerror = (error) => { + logger.error("Video element error:", error); + reject(new Error("Video element error")); + }; }); } else { logger.error("Video element not found"); @@ -672,14 +696,31 @@ export default class ImageMethodDialog extends Vue { } stopCameraPreview() { + logger.debug("stopCameraPreview called"); + if (this.cameraStream) { - this.cameraStream.getTracks().forEach((track) => track.stop()); + logger.debug("Stopping camera stream tracks"); + this.cameraStream.getTracks().forEach((track) => { + track.stop(); + logger.debug(`Stopped track: ${track.kind} - ${track.label}`); + }); this.cameraStream = null; } + + // Clear video element srcObject to ensure cleanup + const videoElement = this.$refs.videoElement as HTMLVideoElement; + if (videoElement) { + videoElement.srcObject = null; + logger.debug("Cleared video element srcObject"); + } + this.showCameraPreview = false; this.cameraState = "off"; this.cameraStateMessage = "Camera stopped"; this.error = null; + // Don't reset isRetrying here - let the calling method handle it + + logger.debug("Camera preview stopped and cleaned up"); } async capturePhoto() { @@ -746,10 +787,42 @@ export default class ImageMethodDialog extends Vue { } async retryImage() { + // Set retry flag FIRST to prevent error message flash + this.isRetrying = true; + this.blob = undefined; - if (!this.platformCapabilities.isNativeApp) { - await this.startCameraPreview(); + this.showRetry = true; + + // Stop camera stream but keep showCameraPreview true to prevent error flash + if (this.cameraStream) { + logger.debug("Stopping camera stream tracks"); + this.cameraStream.getTracks().forEach((track) => { + track.stop(); + logger.debug(`Stopped track: ${track.kind} - ${track.label}`); + }); + this.cameraStream = null; } + + // Clear video element srcObject to ensure cleanup + const videoElement = this.$refs.videoElement as HTMLVideoElement; + if (videoElement) { + videoElement.srcObject = null; + logger.debug("Cleared video element srcObject"); + } + + this.cameraState = "off"; + this.cameraStateMessage = "Camera stopped"; + this.error = null; + + // Add a small delay to ensure cleanup is complete + await new Promise((resolve) => setTimeout(resolve, 100)); + + // For native apps (iOS/Android), we need to restart the camera preview + // For web browsers, we also restart the camera preview + await this.startCameraPreview(); + + // Reset retry flag after camera is started + this.isRetrying = false; } async uploadImage() {