@capgo/camera-preview
Version:
1,108 lines (1,101 loc) • 47.7 kB
JavaScript
;
var core = require('@capacitor/core');
exports.DeviceType = void 0;
(function (DeviceType) {
DeviceType["ULTRA_WIDE"] = "ultraWide";
DeviceType["WIDE_ANGLE"] = "wideAngle";
DeviceType["TELEPHOTO"] = "telephoto";
DeviceType["TRUE_DEPTH"] = "trueDepth";
DeviceType["DUAL"] = "dual";
DeviceType["DUAL_WIDE"] = "dualWide";
DeviceType["TRIPLE"] = "triple";
})(exports.DeviceType || (exports.DeviceType = {}));
const CameraPreview = core.registerPlugin('CameraPreview', {
web: () => Promise.resolve().then(function () { return web; }).then((m) => new m.CameraPreviewWeb()),
});
async function getBase64FromFilePath(filePath) {
const url = core.Capacitor.convertFileSrc(filePath);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to read file at path: ${filePath} (status ${response.status})`);
}
const blob = await response.blob();
return await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
const dataUrl = reader.result;
const commaIndex = dataUrl.indexOf(',');
resolve(commaIndex >= 0 ? dataUrl.substring(commaIndex + 1) : dataUrl);
};
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(blob);
});
}
async function deleteFile(path) {
// Use native bridge to delete file to handle platform-specific permissions/URIs
const { success } = await CameraPreview.deleteFile({ path });
return !!success;
}
const DEFAULT_VIDEO_ID = 'capgo_video';
class CameraPreviewWeb extends core.WebPlugin {
constructor() {
super();
/**
* track which camera is used based on start options
* used in capture
*/
this.isBackCamera = false;
this.currentDeviceId = null;
this.videoElement = null;
this.isStarted = false;
this.orientationListenerBound = false;
}
async checkPermissions(options) {
const result = {
camera: 'prompt',
};
const permissionsApi = navigator === null || navigator === void 0 ? void 0 : navigator.permissions;
if (permissionsApi === null || permissionsApi === void 0 ? void 0 : permissionsApi.query) {
try {
const cameraPermission = await permissionsApi.query({ name: 'camera' });
result.camera = this.mapWebPermission(cameraPermission.state);
}
catch (error) {
console.warn('Camera permission query failed', error);
}
if ((options === null || options === void 0 ? void 0 : options.disableAudio) === false) {
try {
const microphonePermission = await permissionsApi.query({
name: 'microphone',
});
result.microphone = this.mapWebPermission(microphonePermission.state);
}
catch (error) {
console.warn('Microphone permission query failed', error);
result.microphone = 'prompt';
}
}
}
else if ((options === null || options === void 0 ? void 0 : options.disableAudio) === false) {
result.microphone = 'prompt';
}
return result;
}
async requestPermissions(options) {
var _a, _b;
const disableAudio = (_a = options === null || options === void 0 ? void 0 : options.disableAudio) !== null && _a !== void 0 ? _a : true;
if ((_b = navigator === null || navigator === void 0 ? void 0 : navigator.mediaDevices) === null || _b === void 0 ? void 0 : _b.getUserMedia) {
const constraints = disableAudio ? { video: true } : { video: true, audio: true };
let stream;
try {
stream = await navigator.mediaDevices.getUserMedia(constraints);
}
catch (error) {
console.warn('Unable to obtain camera or microphone stream', error);
}
finally {
try {
stream === null || stream === void 0 ? void 0 : stream.getTracks().forEach((t) => t.stop());
}
catch (_e) {
/* no-op */
}
}
}
const status = await this.checkPermissions({ disableAudio: disableAudio });
if (options === null || options === void 0 ? void 0 : options.showSettingsAlert) {
console.warn('showSettingsAlert is not supported on the web platform; returning permission status only.');
}
return status;
}
mapWebPermission(state) {
switch (state) {
case 'granted':
return 'granted';
case 'denied':
return 'denied';
case 'prompt':
default:
return 'prompt';
}
}
getCurrentOrientation() {
var _a, _b;
try {
const so = screen.orientation;
const type = (so === null || so === void 0 ? void 0 : so.type) || (so === null || so === void 0 ? void 0 : so.mozOrientation) || (so === null || so === void 0 ? void 0 : so.msOrientation);
if (typeof type === 'string') {
if (type.includes('portrait-primary'))
return 'portrait';
if (type.includes('portrait-secondary'))
return 'portrait-upside-down';
if (type.includes('landscape-primary'))
return 'landscape-left';
if (type.includes('landscape-secondary'))
return 'landscape-right';
if (type.includes('landscape'))
return 'landscape-right'; // avoid generic landscape
if (type.includes('portrait'))
return 'portrait';
}
const angle = window.orientation;
if (typeof angle === 'number') {
if (angle === 0)
return 'portrait';
if (angle === 180)
return 'portrait-upside-down';
if (angle === 90)
return 'landscape-right';
if (angle === -90)
return 'landscape-left';
if (angle === 270)
return 'landscape-left';
}
if ((_a = window.matchMedia('(orientation: portrait)')) === null || _a === void 0 ? void 0 : _a.matches) {
return 'portrait';
}
if ((_b = window.matchMedia('(orientation: landscape)')) === null || _b === void 0 ? void 0 : _b.matches) {
// Default to landscape-right when we can't distinguish primary/secondary
return 'landscape-right';
}
}
catch (e) {
console.error(e);
}
return 'unknown';
}
ensureOrientationListener() {
if (this.orientationListenerBound)
return;
const emit = () => {
this.notifyListeners('orientationChange', {
orientation: this.getCurrentOrientation(),
});
};
window.addEventListener('orientationchange', emit);
window.addEventListener('resize', emit);
this.orientationListenerBound = true;
}
async getOrientation() {
return { orientation: this.getCurrentOrientation() };
}
getSafeAreaInsets() {
throw new Error('Method not implemented.');
}
async getZoomButtonValues() {
throw new Error('getZoomButtonValues not supported under the web platform');
}
async getSupportedPictureSizes() {
throw new Error('getSupportedPictureSizes not supported under the web platform');
}
async start(options) {
if (options.aspectRatio && (options.width || options.height)) {
throw new Error('Cannot set both aspectRatio and size (width/height). Use setPreviewSize after start.');
}
if (this.isStarted && !options.force) {
throw new Error('camera already started');
}
// If force is true and camera is already started, stop it first
if (this.isStarted && options.force) {
await this.stop({ force: true });
}
this.isBackCamera = true;
this.isStarted = false;
const parent = document.getElementById((options === null || options === void 0 ? void 0 : options.parent) || '');
const gridMode = (options === null || options === void 0 ? void 0 : options.gridMode) || 'none';
const positioning = (options === null || options === void 0 ? void 0 : options.positioning) || 'top';
if (options.position) {
this.isBackCamera = options.position === 'rear';
}
const video = document.getElementById(DEFAULT_VIDEO_ID);
if (video) {
video.remove();
}
const container = options.parent ? document.getElementById(options.parent) : document.body;
if (!container) {
throw new Error('container not found');
}
this.videoElement = document.createElement('video');
this.videoElement.id = DEFAULT_VIDEO_ID;
this.videoElement.className = options.className || '';
this.videoElement.playsInline = true;
this.videoElement.muted = true;
this.videoElement.autoplay = true;
// Remove objectFit as we'll match camera's native aspect ratio
this.videoElement.style.backgroundColor = 'transparent';
// Reset any default margins that might interfere
this.videoElement.style.margin = '0';
this.videoElement.style.padding = '0';
container.appendChild(this.videoElement);
if (options.toBack) {
this.videoElement.style.zIndex = '-1';
}
// Default to 4:3 if no aspect ratio or size specified
const useDefaultAspectRatio = !options.aspectRatio && !options.width && !options.height;
const effectiveAspectRatio = options.aspectRatio || (useDefaultAspectRatio ? '4:3' : null);
if (options.width) {
this.videoElement.width = options.width;
this.videoElement.style.width = `${options.width}px`;
}
if (options.height) {
this.videoElement.height = options.height;
this.videoElement.style.height = `${options.height}px`;
}
// Handle positioning - center if x or y not provided
const centerX = options.x === undefined;
const centerY = options.y === undefined;
// Always set position to absolute for proper positioning
this.videoElement.style.position = 'absolute';
console.log('Initial positioning flags:', {
centerX,
centerY,
x: options.x,
y: options.y,
});
if (options.x !== undefined) {
this.videoElement.style.left = `${options.x}px`;
}
if (options.y !== undefined) {
this.videoElement.style.top = `${options.y}px`;
}
// Create and add grid overlay if needed
if (gridMode !== 'none') {
const gridOverlay = this.createGridOverlay(gridMode);
gridOverlay.id = 'camera-grid-overlay';
parent === null || parent === void 0 ? void 0 : parent.appendChild(gridOverlay);
}
// Aspect ratio handling is now done after getting camera stream
// Store centering flags for later use
const needsCenterX = centerX;
const needsCenterY = centerY;
console.log('Centering flags stored:', { needsCenterX, needsCenterY });
// First get the camera stream with basic constraints
const constraints = {
video: {
facingMode: this.isBackCamera ? 'environment' : 'user',
},
};
const stream = await navigator.mediaDevices.getUserMedia(constraints);
if (!stream) {
throw new Error('could not acquire stream');
}
if (!this.videoElement) {
throw new Error('video element not found');
}
// Get the actual camera dimensions from the video track
const videoTrack = stream.getVideoTracks()[0];
const settings = videoTrack.getSettings();
const cameraWidth = settings.width || 640;
const cameraHeight = settings.height || 480;
const cameraAspectRatio = cameraWidth / cameraHeight;
console.log('Camera native dimensions:', {
width: cameraWidth,
height: cameraHeight,
aspectRatio: cameraAspectRatio,
});
console.log('Container dimensions:', {
width: container.offsetWidth,
height: container.offsetHeight,
id: container.id,
});
// Now adjust video element size based on camera's native aspect ratio
if (!options.width && !options.height && !options.aspectRatio) {
// No size specified, fit camera view within container bounds
const containerWidth = container.offsetWidth || window.innerWidth;
const containerHeight = container.offsetHeight || window.innerHeight;
// Calculate dimensions that fit within container while maintaining camera aspect ratio
let targetWidth, targetHeight;
// Try fitting to container width first
targetWidth = containerWidth;
targetHeight = targetWidth / cameraAspectRatio;
// If height exceeds container, fit to height instead
if (targetHeight > containerHeight) {
targetHeight = containerHeight;
targetWidth = targetHeight * cameraAspectRatio;
}
console.log('Video element dimensions:', {
width: targetWidth,
height: targetHeight,
container: { width: containerWidth, height: containerHeight },
});
this.videoElement.width = targetWidth;
this.videoElement.height = targetHeight;
this.videoElement.style.width = `${targetWidth}px`;
this.videoElement.style.height = `${targetHeight}px`;
// Center the video element within its parent container
if (needsCenterX || options.x === undefined) {
const x = Math.round((containerWidth - targetWidth) / 2);
this.videoElement.style.left = `${x}px`;
}
if (needsCenterY || options.y === undefined) {
let y;
switch (positioning) {
case 'top':
y = 0;
break;
case 'bottom':
y = window.innerHeight - targetHeight;
break;
case 'center':
default:
y = Math.round((window.innerHeight - targetHeight) / 2);
break;
}
this.videoElement.style.setProperty('top', `${y}px`, 'important');
// Force a style recalculation
this.videoElement.offsetHeight;
console.log('Positioning video:', {
positioning,
viewportHeight: window.innerHeight,
targetHeight,
calculatedY: y,
actualTop: this.videoElement.style.top,
position: this.videoElement.style.position,
});
}
}
else if (effectiveAspectRatio && !options.width && !options.height) {
// Aspect ratio specified but no size
const [widthRatio, heightRatio] = effectiveAspectRatio.split(':').map(Number);
const targetRatio = widthRatio / heightRatio;
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
let targetWidth, targetHeight;
// Try fitting to viewport width first
targetWidth = viewportWidth;
targetHeight = targetWidth / targetRatio;
// If height exceeds viewport, fit to height instead
if (targetHeight > viewportHeight) {
targetHeight = viewportHeight;
targetWidth = targetHeight * targetRatio;
}
this.videoElement.width = targetWidth;
this.videoElement.height = targetHeight;
this.videoElement.style.width = `${targetWidth}px`;
this.videoElement.style.height = `${targetHeight}px`;
// Center the video element within its parent container
if (needsCenterX || options.x === undefined) {
const parentWidth = container.offsetWidth || viewportWidth;
const x = Math.round((parentWidth - targetWidth) / 2);
this.videoElement.style.left = `${x}px`;
}
if (needsCenterY || options.y === undefined) {
const parentHeight = container.offsetHeight || viewportHeight;
let y;
switch (positioning) {
case 'top':
y = 0;
break;
case 'bottom':
y = parentHeight - targetHeight;
break;
case 'center':
default:
y = Math.round((parentHeight - targetHeight) / 2);
break;
}
this.videoElement.style.top = `${y}px`;
}
}
this.videoElement.srcObject = stream;
if (!this.isBackCamera) {
this.videoElement.style.transform = 'scaleX(-1)';
}
// Set initial zoom level if specified and supported
if (options.initialZoomLevel !== undefined && options.initialZoomLevel !== 1.0) {
// videoTrack already declared above
if (videoTrack) {
const capabilities = videoTrack.getCapabilities();
if (capabilities.zoom) {
const zoomLevel = options.initialZoomLevel;
const minZoom = capabilities.zoom.min || 1;
const maxZoom = capabilities.zoom.max || 1;
if (zoomLevel < minZoom || zoomLevel > maxZoom) {
stream.getTracks().forEach((track) => track.stop());
throw new Error(`Initial zoom level ${zoomLevel} is not available. Valid range is ${minZoom} to ${maxZoom}`);
}
try {
await videoTrack.applyConstraints({
advanced: [{ zoom: zoomLevel }],
});
}
catch (error) {
console.warn(`Failed to set initial zoom level: ${error}`);
// Don't throw, just continue without zoom
}
}
}
}
this.isStarted = true;
this.ensureOrientationListener();
// Wait for video to be ready and get actual dimensions
await new Promise((resolve) => {
const videoEl = this.videoElement;
if (!videoEl) {
throw new Error('video element not found');
}
if (videoEl.readyState >= 2) {
resolve();
}
else {
videoEl.addEventListener('loadeddata', () => resolve(), {
once: true,
});
}
});
// Ensure centering is applied after DOM updates
await new Promise((resolve) => requestAnimationFrame(resolve));
console.log('About to re-center, flags:', { needsCenterX, needsCenterY });
// Re-apply centering with correct parent dimensions
if (needsCenterX) {
const parentWidth = container.offsetWidth;
const x = Math.round((parentWidth - this.videoElement.offsetWidth) / 2);
this.videoElement.style.left = `${x}px`;
console.log('Re-centering X:', {
parentWidth,
videoWidth: this.videoElement.offsetWidth,
x,
});
}
if (needsCenterY) {
let y;
switch (positioning) {
case 'top':
y = 0;
break;
case 'bottom':
y = window.innerHeight - this.videoElement.offsetHeight;
break;
case 'center':
default:
y = Math.round((window.innerHeight - this.videoElement.offsetHeight) / 2);
break;
}
this.videoElement.style.setProperty('top', `${y}px`, 'important');
console.log('Re-positioning Y:', {
positioning,
viewportHeight: window.innerHeight,
videoHeight: this.videoElement.offsetHeight,
y,
position: this.videoElement.style.position,
top: this.videoElement.style.top,
});
}
// Get the actual rendered dimensions after video is loaded
const rect = this.videoElement.getBoundingClientRect();
const computedStyle = window.getComputedStyle(this.videoElement);
console.log('Final video element state:', {
rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
style: {
position: computedStyle.position,
left: computedStyle.left,
top: computedStyle.top,
width: computedStyle.width,
height: computedStyle.height,
},
});
return {
width: Math.round(rect.width),
height: Math.round(rect.height),
x: Math.round(rect.x),
y: Math.round(rect.y),
};
}
stopStream(stream) {
if (stream) {
const tracks = stream.getTracks();
for (const track of tracks)
track.stop();
}
}
async stop(_options) {
const video = document.getElementById(DEFAULT_VIDEO_ID);
if (video) {
video.pause();
this.stopStream(video.srcObject);
video.remove();
this.isStarted = false;
}
// Remove grid overlay if it exists
const gridOverlay = document.getElementById('camera-grid-overlay');
gridOverlay === null || gridOverlay === void 0 ? void 0 : gridOverlay.remove();
}
async capture(options) {
return new Promise((resolve, reject) => {
const video = document.getElementById(DEFAULT_VIDEO_ID);
if (!(video === null || video === void 0 ? void 0 : video.srcObject)) {
reject(new Error('camera is not running'));
return;
}
// video.width = video.offsetWidth;
let base64EncodedImage;
if (video && video.videoWidth > 0 && video.videoHeight > 0) {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
// Calculate capture dimensions
let captureWidth = video.videoWidth;
let captureHeight = video.videoHeight;
const sourceX = 0;
const sourceY = 0;
// If width or height is specified, resize to fit within both maximums while maintaining aspect ratio
if (options.width || options.height) {
const originalAspectRatio = video.videoWidth / video.videoHeight;
const targetWidth = options.width || video.videoWidth;
const targetHeight = options.height || video.videoHeight;
// Calculate dimensions that fit within both maximums
if (options.width && options.height) {
// Both dimensions specified - fit within both
const maxAspectRatio = targetWidth / targetHeight;
if (originalAspectRatio > maxAspectRatio) {
// Original is wider - fit by width
captureWidth = targetWidth;
captureHeight = targetWidth / originalAspectRatio;
}
else {
// Original is taller - fit by height
captureWidth = targetHeight * originalAspectRatio;
captureHeight = targetHeight;
}
}
else if (options.width) {
// Only width specified - maintain aspect ratio
captureWidth = targetWidth;
captureHeight = targetWidth / originalAspectRatio;
}
else {
// Only height specified - maintain aspect ratio
captureWidth = targetHeight * originalAspectRatio;
captureHeight = targetHeight;
}
}
canvas.width = captureWidth;
canvas.height = captureHeight;
// flip horizontally back camera isn't used
if (!this.isBackCamera) {
context === null || context === void 0 ? void 0 : context.translate(captureWidth, 0);
context === null || context === void 0 ? void 0 : context.scale(-1, 1);
}
context === null || context === void 0 ? void 0 : context.drawImage(video, sourceX, sourceY, captureWidth, captureHeight, 0, 0, captureWidth, captureHeight);
if (options.saveToGallery) ;
if (options.withExifLocation) ;
if ((options.format || 'jpeg') === 'jpeg') {
base64EncodedImage = canvas
.toDataURL('image/jpeg', (options.quality || 85) / 100.0)
.replace('data:image/jpeg;base64,', '');
}
else {
base64EncodedImage = canvas.toDataURL('image/png').replace('data:image/png;base64,', '');
}
}
resolve({
value: base64EncodedImage,
exif: {},
});
});
}
async captureSample(_options) {
return this.capture(_options);
}
async stopRecordVideo() {
throw new Error('stopRecordVideo not supported under the web platform');
}
async startRecordVideo(_options) {
console.log('startRecordVideo', _options);
throw new Error('startRecordVideo not supported under the web platform');
}
async getSupportedFlashModes() {
throw new Error('getSupportedFlashModes not supported under the web platform');
}
async getHorizontalFov() {
throw new Error('getHorizontalFov not supported under the web platform');
}
async setFlashMode(_options) {
throw new Error(`setFlashMode not supported under the web platform${_options}`);
}
async flip() {
const video = document.getElementById(DEFAULT_VIDEO_ID);
if (!(video === null || video === void 0 ? void 0 : video.srcObject)) {
throw new Error('camera is not running');
}
// Stop current stream
this.stopStream(video.srcObject);
// Toggle camera position
this.isBackCamera = !this.isBackCamera;
// Get new constraints
const constraints = {
video: {
facingMode: this.isBackCamera ? 'environment' : 'user',
width: { ideal: video.videoWidth || 640 },
height: { ideal: video.videoHeight || 480 },
},
};
try {
const stream = await navigator.mediaDevices.getUserMedia(constraints);
video.srcObject = stream;
// Update current device ID from the new stream
const videoTrack = stream.getVideoTracks()[0];
if (videoTrack) {
this.currentDeviceId = videoTrack.getSettings().deviceId || null;
}
// Update video transform based on camera
if (this.isBackCamera) {
video.style.transform = 'none';
video.style.webkitTransform = 'none';
}
else {
video.style.transform = 'scaleX(-1)';
video.style.webkitTransform = 'scaleX(-1)';
}
await video.play();
}
catch (error) {
throw new Error(`Failed to flip camera: ${error}`);
}
}
async setOpacity(_options) {
const video = document.getElementById(DEFAULT_VIDEO_ID);
if (!!video && !!_options.opacity)
video.style.setProperty('opacity', _options.opacity.toString());
}
async isRunning() {
const video = document.getElementById(DEFAULT_VIDEO_ID);
return { isRunning: !!video && !!video.srcObject };
}
async getAvailableDevices() {
var _a;
if (!((_a = navigator.mediaDevices) === null || _a === void 0 ? void 0 : _a.enumerateDevices)) {
throw new Error('getAvailableDevices not supported under the web platform');
}
const devices = await navigator.mediaDevices.enumerateDevices();
const videoDevices = devices.filter((device) => device.kind === 'videoinput');
// Group devices by position (front/back)
const frontDevices = [];
const backDevices = [];
videoDevices.forEach((device, index) => {
const label = device.label || `Camera ${index + 1}`;
const labelLower = label.toLowerCase();
// Determine device type based on label
let deviceType = exports.DeviceType.WIDE_ANGLE;
let baseZoomRatio = 1.0;
if (labelLower.includes('ultra') || labelLower.includes('0.5')) {
deviceType = exports.DeviceType.ULTRA_WIDE;
baseZoomRatio = 0.5;
}
else if (labelLower.includes('telephoto') ||
labelLower.includes('tele') ||
labelLower.includes('2x') ||
labelLower.includes('3x')) {
deviceType = exports.DeviceType.TELEPHOTO;
baseZoomRatio = 2.0;
}
else if (labelLower.includes('depth') || labelLower.includes('truedepth')) {
deviceType = exports.DeviceType.TRUE_DEPTH;
baseZoomRatio = 1.0;
}
const lensInfo = {
deviceId: device.deviceId,
label,
deviceType,
focalLength: 4.25,
baseZoomRatio,
minZoom: 1.0,
maxZoom: 1.0,
};
// Determine position and add to appropriate array
if (labelLower.includes('back') || labelLower.includes('rear')) {
backDevices.push(lensInfo);
}
else {
frontDevices.push(lensInfo);
}
});
const result = [];
if (frontDevices.length > 0) {
result.push({
deviceId: frontDevices[0].deviceId,
label: 'Front Camera',
position: 'front',
lenses: frontDevices,
isLogical: false,
minZoom: Math.min(...frontDevices.map((d) => d.minZoom)),
maxZoom: Math.max(...frontDevices.map((d) => d.maxZoom)),
});
}
if (backDevices.length > 0) {
result.push({
deviceId: backDevices[0].deviceId,
label: 'Back Camera',
position: 'rear',
lenses: backDevices,
isLogical: false,
minZoom: Math.min(...backDevices.map((d) => d.minZoom)),
maxZoom: Math.max(...backDevices.map((d) => d.maxZoom)),
});
}
return { devices: result };
}
async getZoom() {
const video = document.getElementById(DEFAULT_VIDEO_ID);
if (!(video === null || video === void 0 ? void 0 : video.srcObject)) {
throw new Error('camera is not running');
}
const stream = video.srcObject;
const videoTrack = stream.getVideoTracks()[0];
if (!videoTrack) {
throw new Error('no video track found');
}
const capabilities = videoTrack.getCapabilities();
const settings = videoTrack.getSettings();
if (!capabilities.zoom) {
throw new Error('zoom not supported by this device');
}
// Get current device info to determine lens type
let deviceType = exports.DeviceType.WIDE_ANGLE;
let baseZoomRatio = 1.0;
if (this.currentDeviceId) {
const devices = await navigator.mediaDevices.enumerateDevices();
const device = devices.find((d) => d.deviceId === this.currentDeviceId);
if (device) {
const labelLower = device.label.toLowerCase();
if (labelLower.includes('ultra') || labelLower.includes('0.5')) {
deviceType = exports.DeviceType.ULTRA_WIDE;
baseZoomRatio = 0.5;
}
else if (labelLower.includes('telephoto') ||
labelLower.includes('tele') ||
labelLower.includes('2x') ||
labelLower.includes('3x')) {
deviceType = exports.DeviceType.TELEPHOTO;
baseZoomRatio = 2.0;
}
else if (labelLower.includes('depth') || labelLower.includes('truedepth')) {
deviceType = exports.DeviceType.TRUE_DEPTH;
baseZoomRatio = 1.0;
}
}
}
const currentZoom = settings.zoom || 1;
const lensInfo = {
focalLength: 4.25,
deviceType,
baseZoomRatio,
digitalZoom: currentZoom / baseZoomRatio,
};
return {
min: capabilities.zoom.min || 1,
max: capabilities.zoom.max || 1,
current: currentZoom,
lens: lensInfo,
};
}
async setZoom(options) {
const video = document.getElementById(DEFAULT_VIDEO_ID);
if (!(video === null || video === void 0 ? void 0 : video.srcObject)) {
throw new Error('camera is not running');
}
const stream = video.srcObject;
const videoTrack = stream.getVideoTracks()[0];
if (!videoTrack) {
throw new Error('no video track found');
}
const capabilities = videoTrack.getCapabilities();
if (!capabilities.zoom) {
throw new Error('zoom not supported by this device');
}
const zoomLevel = Math.max(capabilities.zoom.min || 1, Math.min(capabilities.zoom.max || 1, options.level));
// Note: autoFocus is not supported on web platform
try {
await videoTrack.applyConstraints({
advanced: [{ zoom: zoomLevel }],
});
}
catch (error) {
throw new Error(`Failed to set zoom: ${error}`);
}
}
async getFlashMode() {
throw new Error('getFlashMode not supported under the web platform');
}
async getDeviceId() {
return { deviceId: this.currentDeviceId || '' };
}
async setDeviceId(options) {
const video = document.getElementById(DEFAULT_VIDEO_ID);
if (!(video === null || video === void 0 ? void 0 : video.srcObject)) {
throw new Error('camera is not running');
}
// Stop current stream
this.stopStream(video.srcObject);
// Update current device ID
this.currentDeviceId = options.deviceId;
// Get new constraints with specific device ID
const constraints = {
video: {
deviceId: { exact: options.deviceId },
width: { ideal: video.videoWidth || 640 },
height: { ideal: video.videoHeight || 480 },
},
};
try {
// Try to determine camera position from device
const devices = await navigator.mediaDevices.enumerateDevices();
const device = devices.find((d) => d.deviceId === options.deviceId);
this.isBackCamera =
(device === null || device === void 0 ? void 0 : device.label.toLowerCase().includes('back')) || (device === null || device === void 0 ? void 0 : device.label.toLowerCase().includes('rear')) || false;
const stream = await navigator.mediaDevices.getUserMedia(constraints);
video.srcObject = stream;
// Update video transform based on camera
if (this.isBackCamera) {
video.style.transform = 'none';
video.style.webkitTransform = 'none';
}
else {
video.style.transform = 'scaleX(-1)';
video.style.webkitTransform = 'scaleX(-1)';
}
await video.play();
}
catch (error) {
throw new Error(`Failed to swap to device ${options.deviceId}: ${error}`);
}
}
async getAspectRatio() {
const video = document.getElementById(DEFAULT_VIDEO_ID);
if (!video) {
throw new Error('camera is not running');
}
const width = video.offsetWidth;
const height = video.offsetHeight;
if (width && height) {
const ratio = width / height;
// Check for portrait camera ratios: 4:3 -> 3:4, 16:9 -> 9:16
if (Math.abs(ratio - 3 / 4) < 0.01) {
return { aspectRatio: '4:3' };
}
if (Math.abs(ratio - 9 / 16) < 0.01) {
return { aspectRatio: '16:9' };
}
}
// Default to 4:3 if no specific aspect ratio is matched
return { aspectRatio: '4:3' };
}
async setAspectRatio(options) {
const video = document.getElementById(DEFAULT_VIDEO_ID);
if (!video) {
throw new Error('camera is not running');
}
if (options.aspectRatio) {
const [widthRatio, heightRatio] = options.aspectRatio.split(':').map(Number);
// For camera, use portrait orientation: 4:3 becomes 3:4, 16:9 becomes 9:16
const ratio = heightRatio / widthRatio;
// Get current position and size
const rect = video.getBoundingClientRect();
const currentWidth = rect.width;
const currentHeight = rect.height;
const currentRatio = currentWidth / currentHeight;
let newWidth;
let newHeight;
if (currentRatio > ratio) {
// Width is larger, fit by height and center horizontally
newWidth = currentHeight * ratio;
newHeight = currentHeight;
}
else {
// Height is larger, fit by width and center vertically
newWidth = currentWidth;
newHeight = currentWidth / ratio;
}
// Calculate position
let x, y;
if (options.x !== undefined && options.y !== undefined) {
// Use provided coordinates, ensuring they stay within screen boundaries
x = Math.max(0, Math.min(options.x, window.innerWidth - newWidth));
y = Math.max(0, Math.min(options.y, window.innerHeight - newHeight));
}
else {
// Auto-center the view
x = (window.innerWidth - newWidth) / 2;
y = (window.innerHeight - newHeight) / 2;
}
video.style.width = `${newWidth}px`;
video.style.height = `${newHeight}px`;
video.style.left = `${x}px`;
video.style.top = `${y}px`;
video.style.position = 'absolute';
const offsetX = newWidth / 8;
const offsetY = newHeight / 8;
return {
width: Math.round(newWidth),
height: Math.round(newHeight),
x: Math.round(x + offsetX),
y: Math.round(y + offsetY),
};
}
else {
video.style.objectFit = 'cover';
const rect = video.getBoundingClientRect();
const offsetX = rect.width / 8;
const offsetY = rect.height / 8;
return {
width: Math.round(rect.width),
height: Math.round(rect.height),
x: Math.round(rect.left + offsetX),
y: Math.round(rect.top + offsetY),
};
}
}
createGridOverlay(gridMode) {
const overlay = document.createElement('div');
overlay.style.position = 'absolute';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.width = '100%';
overlay.style.height = '100%';
overlay.style.pointerEvents = 'none';
overlay.style.zIndex = '10';
const divisions = gridMode === '3x3' ? 3 : 4;
// Create SVG for grid lines
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.style.width = '100%';
svg.style.height = '100%';
svg.style.position = 'absolute';
svg.style.top = '0';
svg.style.left = '0';
// Create grid lines
for (let i = 1; i < divisions; i++) {
// Vertical lines
const verticalLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
verticalLine.setAttribute('x1', `${(i / divisions) * 100}%`);
verticalLine.setAttribute('y1', '0%');
verticalLine.setAttribute('x2', `${(i / divisions) * 100}%`);
verticalLine.setAttribute('y2', '100%');
verticalLine.setAttribute('stroke', 'rgba(255, 255, 255, 0.5)');
verticalLine.setAttribute('stroke-width', '1');
svg.appendChild(verticalLine);
// Horizontal lines
const horizontalLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
horizontalLine.setAttribute('x1', '0%');
horizontalLine.setAttribute('y1', `${(i / divisions) * 100}%`);
horizontalLine.setAttribute('x2', '100%');
horizontalLine.setAttribute('y2', `${(i / divisions) * 100}%`);
horizontalLine.setAttribute('stroke', 'rgba(255, 255, 255, 0.5)');
horizontalLine.setAttribute('stroke-width', '1');
svg.appendChild(horizontalLine);
}
overlay.appendChild(svg);
return overlay;
}
async setGridMode(options) {
// Web implementation of grid mode would need to be implemented
// For now, just resolve as a no-op
console.warn(`Grid mode '${options.gridMode}' is not yet implemented for web platform`);
}
async getGridMode() {
// Web implementation - default to none
return { gridMode: 'none' };
}
async getPreviewSize() {
const video = document.getElementById(DEFAULT_VIDEO_ID);
if (!video) {
throw new Error('camera is not running');
}
const offsetX = video.width / 8;
const offsetY = video.height / 8;
return {
x: video.offsetLeft + offsetX,
y: video.offsetTop + offsetY,
width: video.width,
height: video.height,
};
}
async setPreviewSize(options) {
const video = document.getElementById(DEFAULT_VIDEO_ID);
if (!video) {
throw new Error('camera is not running');
}
video.style.left = `${options.x}px`;
video.style.top = `${options.y}px`;
video.width = options.width;
video.height = options.height;
video.style.width = `${options.width}px`;
video.style.height = `${options.height}px`;
const offsetX = options.width / 8;
const offsetY = options.height / 8;
return {
width: options.width,
height: options.height,
x: options.x + offsetX,
y: options.y + offsetY,
};
}
async setFocus(options) {
// Reject if values are outside 0-1 range
if (options.x < 0 || options.x > 1 || options.y < 0 || options.y > 1) {
throw new Error('Focus coordinates must be between 0 and 1');
}
const video = document.getElementById(DEFAULT_VIDEO_ID);
if (!(video === null || video === void 0 ? void 0 : video.srcObject)) {
throw new Error('camera is not running');
}
const stream = video.srcObject;
const videoTrack = stream.getVideoTracks()[0];
if (!videoTrack) {
throw new Error('no video track found');
}
const capabilities = videoTrack.getCapabilities();
// Check if focusing is supported
if (capabilities.focusMode) {
try {
// Web API supports focus mode settings but not coordinate-based focus
// Setting to manual mode allows for coordinate focus if supported
await videoTrack.applyConstraints({
advanced: [
{
focusMode: 'manual',
focusDistance: 0.5, // Mid-range focus as fallback
},
],
});
}
catch (error) {
console.warn(`setFocus is not fully supported on this device: ${error}. Focus coordinates (${options.x}, ${options.y}) were provided but cannot be applied.`);
}
}
else {
console.warn('Focus control is not supported on this device. Focus coordinates were provided but cannot be applied.');
}
}
// Exposure stubs (unsupported on web)
async getExposureModes() {
throw new Error('getExposureModes not supported under the web platform');
}
async getExposureMode() {
throw new Error('getExposureMode not supported under the web platform');
}
async setExposureMode(_options) {
throw new Error('setExposureMode not supported under the web platform');
}
async getExposureCompensationRange() {
throw new Error('getExposureCompensationRange not supported under the web platform');
}
async getExposureCompensation() {
throw new Error('getExposureCompensation not supported under the web platform');
}
async setExposureCompensation(_options) {
throw new Error('setExposureCompensation not supported under the web platform');
}
async deleteFile(_options) {
throw new Error('deleteFile not supported under the web platform');
}
async getPluginVersion() {
return { version: 'web' };
}
}
var web = /*#__PURE__*/Object.freeze({
__proto__: null,
CameraPreviewWeb: CameraPreviewWeb
});
exports.CameraPreview = CameraPreview;
exports.deleteFile = deleteFile;
exports.getBase64FromFilePath = getBase64FromFilePath;
//# sourceMappingURL=plugin.cjs.js.map