import {Inset} from "../generated/zenid-types.generated.js"; import {CameraFacingMode} from "../../interfaces/verification-system-interfaces.js"; import {zenidLog} from "../zenid-log.js"; export class CameraHelpers { static calculateZoomToView(cameraWidth: number, cameraHeight: number, targetWidth: number, targetHeight: number): Inset { // Input validation if (cameraWidth <= 0 || cameraHeight <= 0 || targetWidth <= 0 || targetHeight <= 0) { return { Left: 0, Top: 0, Right: 0, Bottom: 0 }; } const cameraAspect = cameraWidth / cameraHeight; const targetAspect = targetWidth / targetHeight; // If aspects are nearly identical, no cropping needed if (Math.abs(cameraAspect - targetAspect) < 0.001) { return { Left: 0, Top: 0, Right: 0, Bottom: 0 }; } let inset: Inset = { Left: 0, Top: 0, Right: 0, Bottom: 0 }; if (cameraAspect > targetAspect) { // Camera is wider than target - need to crop horizontally const newWidth = cameraHeight * targetAspect; const cropAmount = cameraWidth - newWidth; inset.Left = Math.floor(cropAmount / 2); inset.Right = Math.ceil(cropAmount / 2); } else { // Camera is taller than target - need to crop vertically const newHeight = cameraWidth / targetAspect; const cropAmount = cameraHeight - newHeight; inset.Top = Math.floor(cropAmount / 2); inset.Bottom = Math.ceil(cropAmount / 2); } return inset; } static getDimensionsInAspectRatio( maxResolution: number, targetAspectRatio: number ): { width: number; height: number } { let width = targetAspectRatio > 1 ? maxResolution : maxResolution * targetAspectRatio; let height = targetAspectRatio > 1 ? maxResolution / targetAspectRatio : maxResolution; return { width, height }; } } /** * Detects the facing mode of a camera based on settings, label, and capabilities * @param {MediaTrackSettings | undefined} settings - The track settings * @param {MediaTrackCapabilities} capabilities - The track capabilities * @param {string} label - The device label * @returns {CameraFacingMode} The detected facing mode */ export function detectCameraFacingMode(settings: MediaTrackSettings | undefined, capabilities: MediaTrackCapabilities, label: string): CameraFacingMode { // Method 1: Check stream settings (most reliable) if (settings?.facingMode) { zenidLog.info(`Detected facing mode from settings: ${settings.facingMode}`); return CameraFacingMode[settings.facingMode as keyof typeof CameraFacingMode] || CameraFacingMode.unknown; } // Method 2: Analyze device label for hints const labelLower = label.toLowerCase(); if (labelLower.includes('front') || labelLower.includes('user') || labelLower.includes('selfie')) { return CameraFacingMode.user; } else if (labelLower.includes('back') || labelLower.includes('rear') || labelLower.includes('environment')) { return CameraFacingMode.environment; } // Method 3: Use capabilities if available const caps = capabilities as any; if (caps.facingMode && caps.facingMode.length > 0) { // If capabilities include 'user', it's likely a front camera return caps.facingMode.includes('user') ? CameraFacingMode.user : CameraFacingMode.environment; } return CameraFacingMode.unknown; } /** * Detects if a camera is a virtual camera based on its label * @param {string} label - The device label * @returns {boolean} True if the camera appears to be virtual */ export function isVirtualCamera(label: string): boolean { const labelLower = label.toLowerCase(); // Common virtual camera software patterns const virtualCameraPatterns = [ 'obs', 'virtual', 'manycam', 'xsplit', 'snap camera', 'snap cam', 'logitech capture', 'nvidia broadcast', 'chromacam', 'vcam', 'droidcam', 'epoccam', 'iriun', 'camo', 'mmhmm', 'streamlabs' ]; return virtualCameraPatterns.some(pattern => labelLower.includes(pattern)); } /** * Scores a device based on its probed capabilities. * @param {MediaTrackCapabilities} trackCapabilities - The track capabilities of the device. * @param {any} photoCapabilities - The photo capabilities of the device. * @param {string} label - The device label. * @returns {number} The calculated score. */ export function scoreDevice(trackCapabilities: MediaTrackCapabilities, photoCapabilities: any, label: string): number { let score = 0; // Cast to any to access non-standard properties const trackCaps = trackCapabilities as any; // Heuristic 1: Torch support (High Confidence) - check both sources const hasTorch = photoCapabilities.fillLightMode?.includes('torch') || photoCapabilities.fillLightMode?.includes('flash') || trackCaps.torch === true; if (hasTorch) { score += 50; // Prioritize cameras with torch support } // Heuristic 2: Focus Mode (Essential Prerequisite) - check both sources const hasContinuousFocus = trackCaps.focusMode?.includes('continuous') || photoCapabilities.focusMode?.includes('continuous'); if (hasContinuousFocus) { score += 20; } else if (!trackCaps.focusMode || trackCaps.focusMode.length === 0 || trackCaps.focusMode.includes('manual')) { score = -100; // Disqualify } // Heuristic 3: Minimum Focus Distance (Fine-tuning) if (trackCaps.focusDistance?.min && trackCaps.focusDistance.min < 0.1) { score += 10; } // Heuristic 4: Zoom (Negative Indicator for Telephoto) const zoom = trackCaps.zoom?.min; if (zoom && zoom > 1.0) { score = -100; // Disqualify, this is a telephoto lens } // Heuristic 5: Resolution (Low Confidence) if (trackCapabilities.width?.max && trackCapabilities.width.max > 1920) { score += 5; } // Heuristic 6: Label (Very Low Confidence, Tie-breaker) const labelLower = label.toLowerCase(); if (labelLower.includes('tele') || labelLower.includes('zoom')) { score -= 5; } // Log the scoring details zenidLog.debug(`Camera ${label} scored ${score} (torch: ${hasTorch}, focus: ${hasContinuousFocus}, zoom: ${zoom || 'N/A'})`); return score; }