|
@ -20,7 +20,7 @@ |
|
|
|
|
|
|
|
|
<!-- FEEDBACK: Show if camera preview is not visible after mounting --> |
|
|
<!-- FEEDBACK: Show if camera preview is not visible after mounting --> |
|
|
<div |
|
|
<div |
|
|
v-if="!showCameraPreview && !blob && isRegistered" |
|
|
v-if="!showCameraPreview && !blob && isRegistered && !isRetrying" |
|
|
class="bg-red-100 text-red-700 border border-red-400 rounded px-4 py-3 my-4 text-sm" |
|
|
class="bg-red-100 text-red-700 border border-red-400 rounded px-4 py-3 my-4 text-sm" |
|
|
> |
|
|
> |
|
|
<strong>Camera preview not started.</strong> |
|
|
<strong>Camera preview not started.</strong> |
|
@ -328,6 +328,9 @@ export default class ImageMethodDialog extends Vue { |
|
|
/** Whether to show camera preview */ |
|
|
/** Whether to show camera preview */ |
|
|
showCameraPreview = false; |
|
|
showCameraPreview = false; |
|
|
|
|
|
|
|
|
|
|
|
/** Whether currently retrying camera preview */ |
|
|
|
|
|
isRetrying = false; |
|
|
|
|
|
|
|
|
/** Camera stream reference */ |
|
|
/** Camera stream reference */ |
|
|
private cameraStream: MediaStream | null = null; |
|
|
private cameraStream: MediaStream | null = null; |
|
|
|
|
|
|
|
@ -589,6 +592,7 @@ export default class ImageMethodDialog extends Vue { |
|
|
} |
|
|
} |
|
|
this.blob = undefined; |
|
|
this.blob = undefined; |
|
|
this.showCameraPreview = false; |
|
|
this.showCameraPreview = false; |
|
|
|
|
|
this.isRetrying = false; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async startCameraPreview() { |
|
|
async startCameraPreview() { |
|
@ -600,6 +604,13 @@ export default class ImageMethodDialog extends Vue { |
|
|
"getUserMedia available:", |
|
|
"getUserMedia available:", |
|
|
!!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia), |
|
|
!!(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 { |
|
|
try { |
|
|
this.cameraState = "initializing"; |
|
|
this.cameraState = "initializing"; |
|
@ -611,10 +622,17 @@ export default class ImageMethodDialog extends Vue { |
|
|
throw new Error("Camera API not available in this browser"); |
|
|
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({ |
|
|
const stream = await navigator.mediaDevices.getUserMedia({ |
|
|
video: { facingMode: this.currentFacingMode }, |
|
|
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.cameraStream = stream; |
|
|
this.cameraState = "active"; |
|
|
this.cameraState = "active"; |
|
|
this.cameraStateMessage = "Camera is active"; |
|
|
this.cameraStateMessage = "Camera is active"; |
|
@ -623,20 +641,26 @@ export default class ImageMethodDialog extends Vue { |
|
|
|
|
|
|
|
|
const videoElement = this.$refs.videoElement as HTMLVideoElement; |
|
|
const videoElement = this.$refs.videoElement as HTMLVideoElement; |
|
|
if (videoElement) { |
|
|
if (videoElement) { |
|
|
|
|
|
logger.debug("Setting video element srcObject"); |
|
|
videoElement.srcObject = stream; |
|
|
videoElement.srcObject = stream; |
|
|
await new Promise((resolve) => { |
|
|
await new Promise((resolve, reject) => { |
|
|
videoElement.onloadedmetadata = () => { |
|
|
videoElement.onloadedmetadata = () => { |
|
|
|
|
|
logger.debug("Video metadata loaded, starting playback"); |
|
|
videoElement |
|
|
videoElement |
|
|
.play() |
|
|
.play() |
|
|
.then(() => { |
|
|
.then(() => { |
|
|
logger.debug("Video element started playing"); |
|
|
logger.debug("Video element started playing successfully"); |
|
|
resolve(true); |
|
|
resolve(true); |
|
|
}) |
|
|
}) |
|
|
.catch((error) => { |
|
|
.catch((error) => { |
|
|
logger.error("Error playing video:", 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 { |
|
|
} else { |
|
|
logger.error("Video element not found"); |
|
|
logger.error("Video element not found"); |
|
@ -672,14 +696,31 @@ export default class ImageMethodDialog extends Vue { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
stopCameraPreview() { |
|
|
stopCameraPreview() { |
|
|
|
|
|
logger.debug("stopCameraPreview called"); |
|
|
|
|
|
|
|
|
if (this.cameraStream) { |
|
|
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; |
|
|
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.showCameraPreview = false; |
|
|
this.cameraState = "off"; |
|
|
this.cameraState = "off"; |
|
|
this.cameraStateMessage = "Camera stopped"; |
|
|
this.cameraStateMessage = "Camera stopped"; |
|
|
this.error = null; |
|
|
this.error = null; |
|
|
|
|
|
// Don't reset isRetrying here - let the calling method handle it |
|
|
|
|
|
|
|
|
|
|
|
logger.debug("Camera preview stopped and cleaned up"); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async capturePhoto() { |
|
|
async capturePhoto() { |
|
@ -746,10 +787,42 @@ export default class ImageMethodDialog extends Vue { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async retryImage() { |
|
|
async retryImage() { |
|
|
|
|
|
// Set retry flag FIRST to prevent error message flash |
|
|
|
|
|
this.isRetrying = true; |
|
|
|
|
|
|
|
|
this.blob = undefined; |
|
|
this.blob = undefined; |
|
|
if (!this.platformCapabilities.isNativeApp) { |
|
|
this.showRetry = true; |
|
|
await this.startCameraPreview(); |
|
|
|
|
|
|
|
|
// 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() { |
|
|
async uploadImage() { |
|
|