You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
195 lines
5.6 KiB
195 lines
5.6 KiB
import { QRScannerService, ScanListener, QRScannerOptions } from "./types";
|
|
import { logger } from "@/utils/logger";
|
|
import { EventEmitter } from "events";
|
|
|
|
export class WebInlineQRScanner implements QRScannerService {
|
|
private scanListener: ScanListener | null = null;
|
|
private isScanning = false;
|
|
private stream: MediaStream | null = null;
|
|
private events = new EventEmitter();
|
|
|
|
constructor(private options?: QRScannerOptions) {}
|
|
|
|
async checkPermissions(): Promise<boolean> {
|
|
try {
|
|
logger.log("[QRScanner] Checking camera permissions...");
|
|
const permissions = await navigator.permissions.query({
|
|
name: "camera" as PermissionName,
|
|
});
|
|
logger.log("[QRScanner] Permission state:", permissions.state);
|
|
return permissions.state === "granted";
|
|
} catch (error) {
|
|
logger.error("[QRScanner] Error checking camera permissions:", error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async requestPermissions(): Promise<boolean> {
|
|
try {
|
|
// First check if we have any video devices
|
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
const videoDevices = devices.filter(
|
|
(device) => device.kind === "videoinput",
|
|
);
|
|
|
|
if (videoDevices.length === 0) {
|
|
logger.error("No video devices found");
|
|
throw new Error("No camera found on this device");
|
|
}
|
|
|
|
// Try to get a stream with specific constraints
|
|
const stream = await navigator.mediaDevices.getUserMedia({
|
|
video: {
|
|
facingMode: "environment",
|
|
width: { ideal: 1280 },
|
|
height: { ideal: 720 },
|
|
},
|
|
});
|
|
|
|
// Stop the test stream immediately
|
|
stream.getTracks().forEach((track) => track.stop());
|
|
return true;
|
|
} catch (error) {
|
|
const wrappedError =
|
|
error instanceof Error ? error : new Error(String(error));
|
|
logger.error("Error requesting camera permissions:", {
|
|
error: wrappedError.message,
|
|
stack: wrappedError.stack,
|
|
name: wrappedError.name,
|
|
});
|
|
|
|
// Provide more specific error messages
|
|
if (
|
|
wrappedError.name === "NotFoundError" ||
|
|
wrappedError.name === "DevicesNotFoundError"
|
|
) {
|
|
throw new Error("No camera found on this device");
|
|
} else if (
|
|
wrappedError.name === "NotAllowedError" ||
|
|
wrappedError.name === "PermissionDeniedError"
|
|
) {
|
|
throw new Error(
|
|
"Camera access denied. Please grant camera permission and try again",
|
|
);
|
|
} else if (
|
|
wrappedError.name === "NotReadableError" ||
|
|
wrappedError.name === "TrackStartError"
|
|
) {
|
|
throw new Error("Camera is in use by another application");
|
|
} else {
|
|
throw new Error(`Camera error: ${wrappedError.message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
async isSupported(): Promise<boolean> {
|
|
try {
|
|
// Check for secure context first
|
|
if (!window.isSecureContext) {
|
|
logger.warn("Camera access requires HTTPS (secure context)");
|
|
return false;
|
|
}
|
|
|
|
// Check for camera API support
|
|
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
logger.warn("Camera API not supported in this browser");
|
|
return false;
|
|
}
|
|
|
|
// Check if we have any video devices
|
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
const hasVideoDevices = devices.some(
|
|
(device) => device.kind === "videoinput",
|
|
);
|
|
|
|
if (!hasVideoDevices) {
|
|
logger.warn("No video devices found");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
} catch (error) {
|
|
logger.error("Error checking camera support:", {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
stack: error instanceof Error ? error.stack : undefined,
|
|
});
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async startScan(): Promise<void> {
|
|
if (this.isScanning) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
this.isScanning = true;
|
|
logger.log("[WebInlineQRScanner] Starting scan");
|
|
|
|
// Get camera stream
|
|
this.stream = await navigator.mediaDevices.getUserMedia({
|
|
video: {
|
|
facingMode: "environment",
|
|
width: { ideal: 1280 },
|
|
height: { ideal: 720 },
|
|
},
|
|
});
|
|
|
|
// Emit stream to component
|
|
this.events.emit("stream", this.stream);
|
|
} catch (error) {
|
|
this.isScanning = false;
|
|
const wrappedError =
|
|
error instanceof Error ? error : new Error(String(error));
|
|
if (this.scanListener?.onError) {
|
|
this.scanListener.onError(wrappedError);
|
|
}
|
|
logger.error("Error starting scan:", wrappedError);
|
|
throw wrappedError;
|
|
}
|
|
}
|
|
|
|
async stopScan(): Promise<void> {
|
|
if (!this.isScanning) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
logger.log("[WebInlineQRScanner] Stopping scan");
|
|
|
|
// Stop all tracks in the stream
|
|
if (this.stream) {
|
|
this.stream.getTracks().forEach((track) => track.stop());
|
|
this.stream = null;
|
|
}
|
|
|
|
// Emit stream stopped event
|
|
this.events.emit("stream", null);
|
|
} catch (error) {
|
|
const wrappedError =
|
|
error instanceof Error ? error : new Error(String(error));
|
|
logger.error("Error stopping scan:", wrappedError);
|
|
throw wrappedError;
|
|
} finally {
|
|
this.isScanning = false;
|
|
}
|
|
}
|
|
|
|
addListener(listener: ScanListener): void {
|
|
this.scanListener = listener;
|
|
}
|
|
|
|
// Add method to get stream events
|
|
onStream(callback: (stream: MediaStream | null) => void): void {
|
|
this.events.on("stream", callback);
|
|
}
|
|
|
|
async cleanup(): Promise<void> {
|
|
try {
|
|
await this.stopScan();
|
|
this.events.removeAllListeners();
|
|
} catch (error) {
|
|
logger.error("Error during cleanup:", error);
|
|
}
|
|
}
|
|
}
|
|
|