← back to Watches
public/camera-scanner.js
367 lines
/**
* Camera Scanner for Watch Reference Number Recognition
* Supports barcode scanning, OCR for reference numbers, and image capture
*/
class WatchScanner {
constructor() {
this.stream = null;
this.videoElement = null;
this.canvasElement = null;
this.isScanning = false;
this.onScanCallback = null;
this.constraints = {
video: {
facingMode: 'environment',
width: { ideal: 1920 },
height: { ideal: 1080 },
focusMode: 'continuous'
}
};
}
/**
* Initialize camera scanner
*/
async init(videoElementId, canvasElementId) {
this.videoElement = document.getElementById(videoElementId);
this.canvasElement = document.getElementById(canvasElementId);
if (!this.videoElement || !this.canvasElement) {
throw new Error('Video or canvas element not found');
}
// Check if camera is available
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
throw new Error('Camera not supported on this device');
}
return this;
}
/**
* Start camera stream
*/
async start() {
try {
this.stream = await navigator.mediaDevices.getUserMedia(this.constraints);
this.videoElement.srcObject = this.stream;
this.videoElement.play();
console.log('[Scanner] Camera started');
// Wait for video to be ready
return new Promise((resolve) => {
this.videoElement.onloadedmetadata = () => {
resolve(this);
};
});
} catch (error) {
console.error('[Scanner] Failed to start camera:', error);
throw new Error('Unable to access camera: ' + error.message);
}
}
/**
* Stop camera stream
*/
stop() {
if (this.stream) {
this.stream.getTracks().forEach(track => track.stop());
this.stream = null;
this.isScanning = false;
console.log('[Scanner] Camera stopped');
}
}
/**
* Capture current frame
*/
captureFrame() {
const context = this.canvasElement.getContext('2d');
this.canvasElement.width = this.videoElement.videoWidth;
this.canvasElement.height = this.videoElement.videoHeight;
context.drawImage(
this.videoElement,
0, 0,
this.canvasElement.width,
this.canvasElement.height
);
return this.canvasElement.toDataURL('image/jpeg', 0.8);
}
/**
* Start continuous scanning for watch reference numbers
*/
startScanning(callback) {
this.isScanning = true;
this.onScanCallback = callback;
const scan = () => {
if (!this.isScanning) return;
try {
const imageData = this.captureFrame();
this.detectWatchReference(imageData);
} catch (error) {
console.error('[Scanner] Scan error:', error);
}
// Scan every 500ms
setTimeout(scan, 500);
};
scan();
console.log('[Scanner] Scanning started');
}
/**
* Stop scanning
*/
stopScanning() {
this.isScanning = false;
console.log('[Scanner] Scanning stopped');
}
/**
* Detect watch reference number from image
* This is a simplified version - in production, you'd use:
* - Tesseract.js for OCR
* - ZXing for barcode scanning
* - Cloud Vision API for better accuracy
*/
async detectWatchReference(imageData) {
try {
// Simulate OCR detection (in production, integrate Tesseract.js)
const mockResult = this.simulateOCR(imageData);
if (mockResult.found && this.onScanCallback) {
this.onScanCallback({
success: true,
referenceNumber: mockResult.reference,
confidence: mockResult.confidence,
imageData: imageData
});
}
} catch (error) {
console.error('[Scanner] Detection error:', error);
}
}
/**
* Simulate OCR (replace with real OCR in production)
*/
simulateOCR(imageData) {
// In production, use Tesseract.js or similar
// For demo purposes, we'll return mock data
return {
found: false,
reference: null,
confidence: 0
};
}
/**
* Search for watch by reference number
*/
async searchByReference(referenceNumber) {
try {
const response = await fetch(`/api/search?q=${encodeURIComponent(referenceNumber)}`);
const data = await response.json();
if (data.results && data.results.length > 0) {
return {
success: true,
watches: data.results,
message: `Found ${data.results.length} matching watch(es)`
};
} else {
return {
success: false,
message: 'No watches found with this reference number'
};
}
} catch (error) {
console.error('[Scanner] Search error:', error);
return {
success: false,
error: error.message
};
}
}
/**
* Save scanned watch to IndexedDB for offline access
*/
async saveScan(scanData) {
try {
const db = await this.openDB();
const tx = db.transaction('scannedWatches', 'readwrite');
const store = tx.objectStore('scannedWatches');
const scan = {
timestamp: Date.now(),
referenceNumber: scanData.referenceNumber,
imageData: scanData.imageData,
confidence: scanData.confidence,
synced: false,
data: scanData
};
await new Promise((resolve, reject) => {
const request = store.add(scan);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
console.log('[Scanner] Scan saved to IndexedDB');
// Register background sync
if ('serviceWorker' in navigator && 'SyncManager' in window) {
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('sync-scanned-watches');
}
return true;
} catch (error) {
console.error('[Scanner] Failed to save scan:', error);
return false;
}
}
/**
* Get scan history
*/
async getScanHistory() {
try {
const db = await this.openDB();
const tx = db.transaction('scannedWatches', 'readonly');
const store = tx.objectStore('scannedWatches');
return new Promise((resolve, reject) => {
const request = store.getAll();
request.onsuccess = () => resolve(request.result.reverse());
request.onerror = () => reject(request.error);
});
} catch (error) {
console.error('[Scanner] Failed to get history:', error);
return [];
}
}
/**
* Switch camera (front/back)
*/
async switchCamera() {
const currentMode = this.constraints.video.facingMode;
this.constraints.video.facingMode = currentMode === 'environment' ? 'user' : 'environment';
this.stop();
await this.start();
console.log('[Scanner] Camera switched to:', this.constraints.video.facingMode);
}
/**
* Enable torch/flashlight (if available)
*/
async toggleTorch() {
try {
const track = this.stream.getVideoTracks()[0];
const capabilities = track.getCapabilities();
if (capabilities.torch) {
const settings = track.getSettings();
await track.applyConstraints({
advanced: [{ torch: !settings.torch }]
});
console.log('[Scanner] Torch toggled');
return true;
} else {
console.warn('[Scanner] Torch not supported');
return false;
}
} catch (error) {
console.error('[Scanner] Torch error:', error);
return false;
}
}
/**
* Take photo with camera
*/
async takePhoto() {
const imageData = this.captureFrame();
// Save to IndexedDB
const db = await this.openDB();
const tx = db.transaction('scannedWatches', 'readwrite');
const store = tx.objectStore('scannedWatches');
const photo = {
timestamp: Date.now(),
imageData: imageData,
type: 'manual_capture'
};
return new Promise((resolve, reject) => {
const request = store.add(photo);
request.onsuccess = () => resolve(imageData);
request.onerror = () => reject(request.error);
});
}
/**
* Open IndexedDB
*/
openDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('OmegaWatchesPWA', 2);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains('scannedWatches')) {
const store = db.createObjectStore('scannedWatches', {
keyPath: 'id',
autoIncrement: true
});
store.createIndex('timestamp', 'timestamp', { unique: false });
store.createIndex('synced', 'synced', { unique: false });
}
};
});
}
/**
* Get camera capabilities
*/
getCapabilities() {
if (!this.stream) return null;
const track = this.stream.getVideoTracks()[0];
return track.getCapabilities();
}
/**
* Get current settings
*/
getSettings() {
if (!this.stream) return null;
const track = this.stream.getVideoTracks()[0];
return track.getSettings();
}
}
// Export for use in other modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = WatchScanner;
}