From 69cdaf681105073488c3fb58cdb9c7e4976aecc4 Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Mon, 23 Mar 2026 18:12:09 -0400 Subject: [PATCH 01/26] feat: add web implementation for takePhoto and chooseFromGallery Context: This code was generated by Claude. --- src/web.ts | 208 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 203 insertions(+), 5 deletions(-) diff --git a/src/web.ts b/src/web.ts index 57f7047..0f6bc38 100644 --- a/src/web.ts +++ b/src/web.ts @@ -1,6 +1,6 @@ import { WebPlugin, CapacitorException } from '@capacitor/core'; -import { CameraSource, CameraDirection } from './definitions'; +import { CameraSource, CameraDirection, MediaType, MediaTypeSelection } from './definitions'; import type { CameraPlugin, GalleryImageOptions, @@ -20,8 +20,15 @@ import type { } from './definitions'; export class CameraWeb extends WebPlugin implements CameraPlugin { - async takePhoto(_options: TakePhotoOptions): Promise { - throw this.unimplemented('takePhoto is not implemented on Web.'); + async takePhoto(options: TakePhotoOptions): Promise { + // eslint-disable-next-line no-async-promise-executor + return new Promise(async (resolve, reject) => { + if (options.webUseInput) { + this.takePhotoCameraInputExperience(options, resolve, reject); + } else { + this.takePhotoCameraExperience(options, resolve, reject); + } + }); } async recordVideo(_options: RecordVideoOptions): Promise { @@ -32,8 +39,11 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { throw this.unimplemented('playVideo is not implemented on Web.'); } - async chooseFromGallery(_options: ChooseFromGalleryOptions): Promise { - throw this.unimplemented('chooseFromGallery is not implemented on web.'); + async chooseFromGallery(options: ChooseFromGalleryOptions): Promise { + // eslint-disable-next-line no-async-promise-executor + return new Promise(async (resolve, reject) => { + this.galleryInputExperience(options, resolve, reject); + }); } async editPhoto(_options: EditPhotoOptions): Promise { @@ -268,6 +278,194 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { }); } + private async takePhotoCameraExperience(options: TakePhotoOptions, resolve: any, reject: any) { + if (customElements.get('pwa-camera-modal')) { + const cameraModal: any = document.createElement('pwa-camera-modal'); + cameraModal.facingMode = options.cameraDirection === CameraDirection.Front ? 'user' : 'environment'; + document.body.appendChild(cameraModal); + try { + await cameraModal.componentOnReady(); + cameraModal.addEventListener('onPhoto', async (e: any) => { + const photo = e.detail; + + if (photo === null) { + reject(new CapacitorException('User cancelled photos app')); + } else if (photo instanceof Error) { + reject(photo); + } else { + resolve(await this._getCameraPhotoAsMediaResult(photo)); + } + + cameraModal.dismiss(); + document.body.removeChild(cameraModal); + }); + + cameraModal.present(); + } catch (e) { + this.takePhotoCameraInputExperience(options, resolve, reject); + } + } else { + console.error( + `Unable to load PWA Element 'pwa-camera-modal'. See the docs: https://capacitorjs.com/docs/web/pwa-elements.`, + ); + this.takePhotoCameraInputExperience(options, resolve, reject); + } + } + + private takePhotoCameraInputExperience(options: TakePhotoOptions, resolve: any, reject: any) { + let input = document.querySelector('#_capacitor-camera-input-takephoto') as HTMLInputElement; + + const cleanup = () => { + input.parentNode?.removeChild(input); + }; + + if (!input) { + input = document.createElement('input') as HTMLInputElement; + input.id = '_capacitor-camera-input-takephoto'; + input.type = 'file'; + input.hidden = true; + document.body.appendChild(input); + input.addEventListener('change', (_e: any) => { + const file = input.files![0]; + let format = 'jpeg'; + + if (file.type === 'image/png') { + format = 'png'; + } else if (file.type === 'image/gif') { + format = 'gif'; + } + + const reader = new FileReader(); + reader.addEventListener('load', () => { + const b64 = (reader.result as string).split(',')[1]; + resolve({ + type: MediaType.Photo, + thumbnail: b64, + webPath: URL.createObjectURL(file), + saved: false, + metadata: { + format, + resolution: '0x0', // Resolution not available from file input + }, + } as MediaResult); + cleanup(); + }); + + reader.readAsDataURL(file); + }); + input.addEventListener('cancel', (_e: any) => { + reject(new CapacitorException('User cancelled photos app')); + cleanup(); + }); + } + + input.accept = 'image/*'; + (input as any).capture = true; + + if (options.cameraDirection === CameraDirection.Front) { + (input as any).capture = 'user'; + } else if (options.cameraDirection === CameraDirection.Rear) { + (input as any).capture = 'environment'; + } + + input.click(); + } + + private galleryInputExperience(options: ChooseFromGalleryOptions, resolve: any, reject: any) { + let input = document.querySelector('#_capacitor-camera-input-gallery') as HTMLInputElement; + + const cleanup = () => { + input.parentNode?.removeChild(input); + }; + + if (!input) { + input = document.createElement('input') as HTMLInputElement; + input.id = '_capacitor-camera-input-gallery'; + input.type = 'file'; + input.hidden = true; + input.multiple = options.allowMultipleSelection ?? false; + document.body.appendChild(input); + input.addEventListener('change', (_e: any) => { + const results: MediaResult[] = []; + const limit = options.limit && options.limit > 0 ? options.limit : input.files!.length; + const filesToProcess = Math.min(limit, input.files!.length); + + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let i = 0; i < filesToProcess; i++) { + const file = input.files![i]; + let format = 'jpeg'; + let type = MediaType.Photo; + + if (file.type.startsWith('image/')) { + type = MediaType.Photo; + if (file.type === 'image/png') { + format = 'png'; + } else if (file.type === 'image/gif') { + format = 'gif'; + } + } else if (file.type.startsWith('video/')) { + type = MediaType.Video; + format = file.type.split('/')[1]; + } + + results.push({ + type, + webPath: URL.createObjectURL(file), + saved: false, + metadata: { + format, + resolution: '0x0', // Resolution not available from file input + }, + }); + } + resolve({ results }); + cleanup(); + }); + input.addEventListener('cancel', (_e: any) => { + reject(new CapacitorException('User cancelled photos app')); + cleanup(); + }); + } + + // Set accept attribute based on mediaType + const mediaType = options.mediaType ?? MediaTypeSelection.Photo; + if (mediaType === MediaTypeSelection.Photo) { + input.accept = 'image/*'; + } else if (mediaType === MediaTypeSelection.Video) { + input.accept = 'video/*'; + } else { + // MediaTypeSelection.All + input.accept = 'image/*,video/*'; + } + + input.click(); + } + + private _getCameraPhotoAsMediaResult(photo: Blob) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + const format = photo.type.split('/')[1]; + reader.readAsDataURL(photo); + reader.onloadend = () => { + const r = reader.result as string; + const b64 = r.split(',')[1]; + resolve({ + type: MediaType.Photo, + thumbnail: b64, + webPath: URL.createObjectURL(photo), + saved: false, + metadata: { + format, + resolution: '0x0', // Resolution not available from blob + }, + }); + }; + reader.onerror = (e) => { + reject(e); + }; + }); + } + async checkPermissions(): Promise { if (typeof navigator === 'undefined' || !navigator.permissions) { throw this.unavailable('Permissions API not available in this browser'); From 6f2504b34670136c42b20ecc39c9603d254c2976 Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Tue, 24 Mar 2026 07:55:09 -0400 Subject: [PATCH 02/26] feat: get resolution for images and videos --- src/web.ts | 66 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 59 insertions(+), 7 deletions(-) diff --git a/src/web.ts b/src/web.ts index 0f6bc38..549d67d 100644 --- a/src/web.ts +++ b/src/web.ts @@ -325,7 +325,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { input.type = 'file'; input.hidden = true; document.body.appendChild(input); - input.addEventListener('change', (_e: any) => { + input.addEventListener('change', async (_e: any) => { const file = input.files![0]; let format = 'jpeg'; @@ -335,6 +335,9 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { format = 'gif'; } + // Get resolution from image file + const resolution = await this._getImageResolution(file); + const reader = new FileReader(); reader.addEventListener('load', () => { const b64 = (reader.result as string).split(',')[1]; @@ -345,7 +348,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { saved: false, metadata: { format, - resolution: '0x0', // Resolution not available from file input + resolution, }, } as MediaResult); cleanup(); @@ -385,7 +388,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { input.hidden = true; input.multiple = options.allowMultipleSelection ?? false; document.body.appendChild(input); - input.addEventListener('change', (_e: any) => { + input.addEventListener('change', async (_e: any) => { const results: MediaResult[] = []; const limit = options.limit && options.limit > 0 ? options.limit : input.files!.length; const filesToProcess = Math.min(limit, input.files!.length); @@ -395,6 +398,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { const file = input.files![i]; let format = 'jpeg'; let type = MediaType.Photo; + let resolution = '0x0'; if (file.type.startsWith('image/')) { type = MediaType.Photo; @@ -403,9 +407,19 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { } else if (file.type === 'image/gif') { format = 'gif'; } + + // Get resolution from image file + resolution = await this._getImageResolution(file); } else if (file.type.startsWith('video/')) { type = MediaType.Video; format = file.type.split('/')[1]; + + // Get resolution from video file + try { + resolution = await this._getVideoResolution(file); + } catch (e) { + console.warn('Failed to get video resolution:', e); + } } results.push({ @@ -414,7 +428,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { saved: false, metadata: { format, - resolution: '0x0', // Resolution not available from file input + resolution, }, }); } @@ -441,10 +455,14 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { input.click(); } - private _getCameraPhotoAsMediaResult(photo: Blob) { - return new Promise((resolve, reject) => { + private async _getCameraPhotoAsMediaResult(photo: Blob): Promise { + return new Promise(async (resolve, reject) => { const reader = new FileReader(); const format = photo.type.split('/')[1]; + + // Get resolution from image blob + const resolution = await this._getImageResolution(photo); + reader.readAsDataURL(photo); reader.onloadend = () => { const r = reader.result as string; @@ -456,7 +474,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { saved: false, metadata: { format, - resolution: '0x0', // Resolution not available from blob + resolution, }, }); }; @@ -466,6 +484,40 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { }); } + private async _getImageResolution(image: Blob | File): Promise { + try { + const bitmap = await createImageBitmap(image); + const resolution = `${bitmap.width}x${bitmap.height}`; + bitmap.close(); + return resolution; + } catch (e) { + console.warn('Failed to get image resolution:', e); + return '0x0'; + } + } + + private _getVideoResolution(videoFile: File): Promise { + return new Promise((resolve) => { + const video = document.createElement('video'); + video.preload = 'metadata'; + + video.onloadedmetadata = () => { + // Clean up + URL.revokeObjectURL(video.src); + const resolution = `${video.videoWidth}x${video.videoHeight}`; + resolve(resolution); + }; + + video.onerror = () => { + // Clean up and return default + URL.revokeObjectURL(video.src); + resolve('0x0'); + }; + + video.src = URL.createObjectURL(videoFile); + }); + } + async checkPermissions(): Promise { if (typeof navigator === 'undefined' || !navigator.permissions) { throw this.unavailable('Permissions API not available in this browser'); From ce960fe43eb3720cfff39925c2bcfdc96e2a2839 Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Tue, 24 Mar 2026 08:08:53 -0400 Subject: [PATCH 03/26] refactor: remove redundant assign --- src/web.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/web.ts b/src/web.ts index 549d67d..5f9cf87 100644 --- a/src/web.ts +++ b/src/web.ts @@ -401,7 +401,6 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { let resolution = '0x0'; if (file.type.startsWith('image/')) { - type = MediaType.Photo; if (file.type === 'image/png') { format = 'png'; } else if (file.type === 'image/gif') { From 96e5abf7be418e010763a6bd995c51126ad71ce4 Mon Sep 17 00:00:00 2001 From: "Alex J." Date: Tue, 24 Mar 2026 08:11:51 -0400 Subject: [PATCH 04/26] fix: use rear camera as default Co-authored-by: Pedro Bilro --- src/web.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/web.ts b/src/web.ts index 5f9cf87..19e8e49 100644 --- a/src/web.ts +++ b/src/web.ts @@ -363,11 +363,10 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { } input.accept = 'image/*'; - (input as any).capture = true; - if (options.cameraDirection === CameraDirection.Front) { (input as any).capture = 'user'; - } else if (options.cameraDirection === CameraDirection.Rear) { + } else { + // CameraDirection.Rear (input as any).capture = 'environment'; } From a05e1df0ad0779be8bf1a0be2ba06695eec8b3ce Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Tue, 24 Mar 2026 08:37:14 -0400 Subject: [PATCH 05/26] feat: add size, creationDate, and duration to new Web functions --- README.md | 6 +++--- src/definitions.ts | 5 +---- src/web.ts | 38 +++++++++++++++++++++++++++++++------- 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index f059a6a..ae19024 100644 --- a/README.md +++ b/README.md @@ -363,11 +363,11 @@ Allows the user to pick multiple pictures from the photo gallery. | Prop | Type | Description | Since | | ------------------ | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | -| **`size`** | number | File size of the media, in bytes Not available on Web. | 8.1.0 | -| **`duration`** | number | Only applicable for `MediaType.Video` - the duration of the media, in seconds. Not available on Web. | 8.1.0 | +| **`size`** | number | File size of the media, in bytes. | 8.1.0 | +| **`duration`** | number | Only applicable for `MediaType.Video` - the duration of the media, in seconds. | 8.1.0 | | **`format`** | string | The format of the image, ex: jpeg, png, mp4. Web supports jpeg, png and gif, but the exact availability may vary depending on the browser. gif is only supported for `chooseFromGallery`, and only if `webUseInput` option is set to `true`. | 8.1.0 | | **`resolution`** | string | The resolution of the media, in `<width>x<height>` format. Example: '1920x1080'. | 8.1.0 | -| **`creationDate`** | string | The date and time the media was created, in ISO 8601 format. If creation date is not available (e.g. Android 7 and below), the last modified date is returned. Not available on web. | 8.1.0 | +| **`creationDate`** | string | The date and time the media was created, in ISO 8601 format. If creation date is not available (e.g. Android 7 and below), the last modified date is returned. | 8.1.0 | | **`exif`** | string | Exif data, if any, retreived from the media item. Not available on Web. | 8.1.0 | diff --git a/src/definitions.ts b/src/definitions.ts index 867de8f..6acd866 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -480,8 +480,7 @@ export interface MediaResult { export interface MediaMetadata { /** - * File size of the media, in bytes - * Not available on Web. + * File size of the media, in bytes. * * @since 8.1.0 */ @@ -489,7 +488,6 @@ export interface MediaMetadata { /** * Only applicable for `MediaType.Video` - the duration of the media, in seconds. - * Not available on Web. * * @since 8.1.0 */ @@ -515,7 +513,6 @@ export interface MediaMetadata { /** * The date and time the media was created, in ISO 8601 format. * If creation date is not available (e.g. Android 7 and below), the last modified date is returned. - * Not available on web. * * @since 8.1.0 */ diff --git a/src/web.ts b/src/web.ts index 19e8e49..70b98ca 100644 --- a/src/web.ts +++ b/src/web.ts @@ -349,6 +349,8 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { metadata: { format, resolution, + size: file.size, + creationDate: new Date(file.lastModified).toISOString(), }, } as MediaResult); cleanup(); @@ -412,12 +414,29 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { type = MediaType.Video; format = file.type.split('/')[1]; - // Get resolution from video file + // Get resolution and duration from video file + let duration: number | undefined; try { - resolution = await this._getVideoResolution(file); + const videoMetadata = await this._getVideoMetadata(file); + resolution = videoMetadata.resolution; + duration = videoMetadata.duration; } catch (e) { - console.warn('Failed to get video resolution:', e); + console.warn('Failed to get video metadata:', e); } + + results.push({ + type, + webPath: URL.createObjectURL(file), + saved: false, + metadata: { + format, + resolution, + size: file.size, + creationDate: new Date(file.lastModified).toISOString(), + duration, + }, + }); + continue; } results.push({ @@ -427,6 +446,8 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { metadata: { format, resolution, + size: file.size, + creationDate: new Date(file.lastModified).toISOString(), }, }); } @@ -473,6 +494,8 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { metadata: { format, resolution, + size: photo.size, + creationDate: new Date().toISOString(), }, }); }; @@ -494,7 +517,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { } } - private _getVideoResolution(videoFile: File): Promise { + private _getVideoMetadata(videoFile: File): Promise<{ resolution: string; duration: number }> { return new Promise((resolve) => { const video = document.createElement('video'); video.preload = 'metadata'; @@ -503,13 +526,14 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { // Clean up URL.revokeObjectURL(video.src); const resolution = `${video.videoWidth}x${video.videoHeight}`; - resolve(resolution); + const duration = video.duration; + resolve({ resolution, duration }); }; video.onerror = () => { - // Clean up and return default + // Clean up and return defaults URL.revokeObjectURL(video.src); - resolve('0x0'); + resolve({ resolution: '0x0', duration: 0 }); }; video.src = URL.createObjectURL(videoFile); From ebdf7ae9f8dd6a8e7a4d482d0c63c55b5b84f27a Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Tue, 24 Mar 2026 09:15:02 -0400 Subject: [PATCH 06/26] fix: remove limit parameter from Web --- README.md | 2 +- src/definitions.ts | 2 +- src/web.ts | 20 +++++++++++++++----- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index ae19024..fdb35a8 100644 --- a/README.md +++ b/README.md @@ -418,7 +418,7 @@ Allows the user to pick multiple pictures from the photo gallery. | ---------------------------- | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | ----- | | **`mediaType`** | MediaTypeSelection | The type of media to select. Can be pictures, videos, or both. | MediaTypeSelection.Photo | 8.1.0 | | **`allowMultipleSelection`** | boolean | Whether or not to allow selecting multiple media files from the gallery. | false | 8.1.0 | -| **`limit`** | number | The maximum number of media files that the user can choose. Only applicable if `allowMultipleSelection` is `true`. Any non-positive number will be treated as unlimited. Note: This option is only supported on Android 13+ and iOS. | 0 | 8.1.0 | +| **`limit`** | number | The maximum number of media files that the user can choose. Only applicable if `allowMultipleSelection` is `true`. Any non-positive number will be treated as unlimited. Note: This option is only supported on Android 13+ and iOS. Not available on Web. | 0 | 8.1.0 | | **`includeMetadata`** | boolean | Whether or not MediaResult should include its metadata. If an error occurs when obtaining the metadata, it will return empty. Note: This option is only supported on Android and iOS. | false | 8.1.0 | | **`allowEdit`** | boolean | Whether to allow the user to crop or make small edits. Only applicable for `MediaTypeSelection.Photo` and `allowMultipleSelection` set to `false`. Note: This option is only supported on Android and iOS. | false | 8.1.0 | | **`editInApp`** | boolean | If `true`, will use an in-app editor for photo edition. If `false`, will open a separate (platform-specific) native app to handle photo edition, falling back to the in-app editor if none is available. Only applicable with `allowEdit` set to true. Note: This option is only supported on Android and iOS. | true | 8.1.0 | diff --git a/src/definitions.ts b/src/definitions.ts index 6acd866..09a267a 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -282,7 +282,7 @@ export interface ChooseFromGalleryOptions { * The maximum number of media files that the user can choose. * Only applicable if `allowMultipleSelection` is `true`. * Any non-positive number will be treated as unlimited. - * Note: This option is only supported on Android 13+ and iOS. + * Note: This option is only supported on Android 13+ and iOS. Not available on Web. * @default 0 * * @since 8.1.0 diff --git a/src/web.ts b/src/web.ts index 70b98ca..c8f6508 100644 --- a/src/web.ts +++ b/src/web.ts @@ -326,7 +326,13 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { input.hidden = true; document.body.appendChild(input); input.addEventListener('change', async (_e: any) => { - const file = input.files![0]; + if (!input.files || input.files.length === 0) { + reject(new CapacitorException('No file selected')); + cleanup(); + return; + } + + const file = input.files[0]; let format = 'jpeg'; if (file.type === 'image/png') { @@ -390,13 +396,17 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { input.multiple = options.allowMultipleSelection ?? false; document.body.appendChild(input); input.addEventListener('change', async (_e: any) => { + if (!input.files || input.files.length === 0) { + reject(new CapacitorException('No files selected')); + cleanup(); + return; + } + const results: MediaResult[] = []; - const limit = options.limit && options.limit > 0 ? options.limit : input.files!.length; - const filesToProcess = Math.min(limit, input.files!.length); // eslint-disable-next-line @typescript-eslint/prefer-for-of - for (let i = 0; i < filesToProcess; i++) { - const file = input.files![i]; + for (let i = 0; i < input.files.length; i++) { + const file = input.files[i]; let format = 'jpeg'; let type = MediaType.Photo; let resolution = '0x0'; From 8db1dbd45e17084994f0d9826e9781b1dfe7b7d6 Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Tue, 24 Mar 2026 09:55:44 -0400 Subject: [PATCH 07/26] fix: add thumbnail to chooseFromGallery, for both images and videos --- README.md | 16 ++++---- src/definitions.ts | 1 + src/web.ts | 98 ++++++++++++++++++++++++++++++++++++---------- 3 files changed, 87 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index fdb35a8..42ef57e 100644 --- a/README.md +++ b/README.md @@ -349,14 +349,14 @@ Allows the user to pick multiple pictures from the photo gallery. #### MediaResult -| Prop | Type | Description | Since | -| --------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | -| **`type`** | MediaType | The type of media result. Either `Photo` or `Video`. | 8.1.0 | -| **`uri`** | string | The URI pointing to the media file. Not available on Web. Use `webPath` instead for Web. | 8.1.0 | -| **`thumbnail`** | string | Returns the thumbnail of the media, base64 encoded. On Web, for `MediaType.Photo`, the full image is returned here, also base64 encoded. | 8.1.0 | -| **`saved`** | boolean | Whether if the media was saved to the gallery successfully or not. Only applicable if `saveToGallery` was set to `true` in input options. Otherwise, `false` is always returned for `save`. Not available on Web. | 8.1.0 | -| **`webPath`** | string | webPath returns a path that can be used to set the src attribute of a media item for efficient loading and rendering. | 8.1.0 | -| **`metadata`** | MediaMetadata | Metadata associated to the media result. Only included if `includeMetadata` was set to `true` in input options. | 8.1.0 | +| Prop | Type | Description | Since | +| --------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | +| **`type`** | MediaType | The type of media result. Either `Photo` or `Video`. | 8.1.0 | +| **`uri`** | string | The URI pointing to the media file. Not available on Web. Use `webPath` instead for Web. | 8.1.0 | +| **`thumbnail`** | string | Returns the thumbnail of the media, base64 encoded. On Web, for `MediaType.Photo`, the full image is returned here, also base64 encoded. On Web, for `MediaType.Video`, a JPEG thumbnail captured from the video is returned. | 8.1.0 | +| **`saved`** | boolean | Whether if the media was saved to the gallery successfully or not. Only applicable if `saveToGallery` was set to `true` in input options. Otherwise, `false` is always returned for `save`. Not available on Web. | 8.1.0 | +| **`webPath`** | string | webPath returns a path that can be used to set the src attribute of a media item for efficient loading and rendering. | 8.1.0 | +| **`metadata`** | MediaMetadata | Metadata associated to the media result. Only included if `includeMetadata` was set to `true` in input options. | 8.1.0 | #### MediaMetadata diff --git a/src/definitions.ts b/src/definitions.ts index 09a267a..fddd998 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -446,6 +446,7 @@ export interface MediaResult { /** * Returns the thumbnail of the media, base64 encoded. * On Web, for `MediaType.Photo`, the full image is returned here, also base64 encoded. + * On Web, for `MediaType.Video`, a JPEG thumbnail captured from the video is returned. * * @since 8.1.0 */ diff --git a/src/web.ts b/src/web.ts index c8f6508..2f17ef2 100644 --- a/src/web.ts +++ b/src/web.ts @@ -420,22 +420,41 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { // Get resolution from image file resolution = await this._getImageResolution(file); + + // Get base64 thumbnail for image + const thumbnail = await this._getBase64FromFile(file); + + results.push({ + type, + thumbnail, + webPath: URL.createObjectURL(file), + saved: false, + metadata: { + format, + resolution, + size: file.size, + creationDate: new Date(file.lastModified).toISOString(), + }, + }); } else if (file.type.startsWith('video/')) { type = MediaType.Video; format = file.type.split('/')[1]; - // Get resolution and duration from video file + // Get resolution, duration, and thumbnail from video file let duration: number | undefined; + let thumbnail: string | undefined; try { const videoMetadata = await this._getVideoMetadata(file); resolution = videoMetadata.resolution; duration = videoMetadata.duration; + thumbnail = videoMetadata.thumbnail; } catch (e) { console.warn('Failed to get video metadata:', e); } results.push({ type, + thumbnail, webPath: URL.createObjectURL(file), saved: false, metadata: { @@ -446,20 +465,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { duration, }, }); - continue; } - - results.push({ - type, - webPath: URL.createObjectURL(file), - saved: false, - metadata: { - format, - resolution, - size: file.size, - creationDate: new Date(file.lastModified).toISOString(), - }, - }); } resolve({ results }); cleanup(); @@ -527,17 +533,69 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { } } - private _getVideoMetadata(videoFile: File): Promise<{ resolution: string; duration: number }> { + private _getBase64FromFile(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = () => { + const dataUrl = reader.result as string; + const base64 = dataUrl.split(',')[1]; + resolve(base64); + }; + reader.onerror = (e) => { + reject(e); + }; + reader.readAsDataURL(file); + }); + } + + private _getVideoMetadata(videoFile: File): Promise<{ resolution: string; duration: number; thumbnail?: string }> { return new Promise((resolve) => { const video = document.createElement('video'); video.preload = 'metadata'; + video.muted = true; video.onloadedmetadata = () => { - // Clean up - URL.revokeObjectURL(video.src); - const resolution = `${video.videoWidth}x${video.videoHeight}`; - const duration = video.duration; - resolve({ resolution, duration }); + // Seek to 1 second or 10% of duration to capture thumbnail + const seekTime = Math.min(1, video.duration * 0.1); + video.currentTime = seekTime; + }; + + video.onseeked = () => { + try { + // Create canvas and capture frame + const canvas = document.createElement('canvas'); + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + const ctx = canvas.getContext('2d'); + + if (ctx) { + ctx.drawImage(video, 0, 0, canvas.width, canvas.height); + const thumbnail = canvas.toDataURL('image/jpeg', 0.8).split(',')[1]; + + // Clean up + URL.revokeObjectURL(video.src); + resolve({ + resolution: `${video.videoWidth}x${video.videoHeight}`, + duration: video.duration, + thumbnail, + }); + } else { + // Clean up and return without thumbnail + URL.revokeObjectURL(video.src); + resolve({ + resolution: `${video.videoWidth}x${video.videoHeight}`, + duration: video.duration, + }); + } + } catch (e) { + console.warn('Failed to generate video thumbnail:', e); + // Clean up and return without thumbnail + URL.revokeObjectURL(video.src); + resolve({ + resolution: `${video.videoWidth}x${video.videoHeight}`, + duration: video.duration, + }); + } }; video.onerror = () => { From 99f89c56db9a3aba0985698399e19b8c7683dfbe Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Tue, 24 Mar 2026 10:22:00 -0400 Subject: [PATCH 08/26] refactor: remove duplicated code --- src/web.ts | 239 ++++++++++++++++++++++++++--------------------------- 1 file changed, 117 insertions(+), 122 deletions(-) diff --git a/src/web.ts b/src/web.ts index 2f17ef2..7f3df78 100644 --- a/src/web.ts +++ b/src/web.ts @@ -93,37 +93,13 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { } private async cameraExperience(options: ImageOptions, resolve: any, reject: any) { - if (customElements.get('pwa-camera-modal')) { - const cameraModal: any = document.createElement('pwa-camera-modal'); - cameraModal.facingMode = options.direction === CameraDirection.Front ? 'user' : 'environment'; - document.body.appendChild(cameraModal); - try { - await cameraModal.componentOnReady(); - cameraModal.addEventListener('onPhoto', async (e: any) => { - const photo = e.detail; - - if (photo === null) { - reject(new CapacitorException('User cancelled photos app')); - } else if (photo instanceof Error) { - reject(photo); - } else { - resolve(await this._getCameraPhoto(photo, options)); - } - - cameraModal.dismiss(); - document.body.removeChild(cameraModal); - }); - - cameraModal.present(); - } catch (e) { - this.fileInputExperience(options, resolve, reject); - } - } else { - console.error( - `Unable to load PWA Element 'pwa-camera-modal'. See the docs: https://capacitorjs.com/docs/web/pwa-elements.`, - ); - this.fileInputExperience(options, resolve, reject); - } + await this._setupPWACameraModal( + options.direction, + (photo) => this._getCameraPhoto(photo, options), + () => this.fileInputExperience(options, resolve, reject), + resolve, + reject + ); } private fileInputExperience(options: ImageOptions, resolve: any, reject: any) { @@ -246,11 +222,11 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { private _getCameraPhoto(photo: Blob, options: ImageOptions) { return new Promise((resolve, reject) => { const reader = new FileReader(); - const format = photo.type.split('/')[1]; + const format = this._getFileFormat(photo); if (options.resultType === 'uri') { resolve({ webPath: URL.createObjectURL(photo), - format: format, + format, saved: false, }); } else { @@ -260,13 +236,13 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { if (options.resultType === 'dataUrl') { resolve({ dataUrl: r, - format: format, + format, saved: false, }); } else { resolve({ base64String: r.split(',')[1], - format: format, + format, saved: false, }); } @@ -279,69 +255,30 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { } private async takePhotoCameraExperience(options: TakePhotoOptions, resolve: any, reject: any) { - if (customElements.get('pwa-camera-modal')) { - const cameraModal: any = document.createElement('pwa-camera-modal'); - cameraModal.facingMode = options.cameraDirection === CameraDirection.Front ? 'user' : 'environment'; - document.body.appendChild(cameraModal); - try { - await cameraModal.componentOnReady(); - cameraModal.addEventListener('onPhoto', async (e: any) => { - const photo = e.detail; - - if (photo === null) { - reject(new CapacitorException('User cancelled photos app')); - } else if (photo instanceof Error) { - reject(photo); - } else { - resolve(await this._getCameraPhotoAsMediaResult(photo)); - } - - cameraModal.dismiss(); - document.body.removeChild(cameraModal); - }); - - cameraModal.present(); - } catch (e) { - this.takePhotoCameraInputExperience(options, resolve, reject); - } - } else { - console.error( - `Unable to load PWA Element 'pwa-camera-modal'. See the docs: https://capacitorjs.com/docs/web/pwa-elements.`, - ); - this.takePhotoCameraInputExperience(options, resolve, reject); - } + await this._setupPWACameraModal( + options.cameraDirection, + (photo) => this._getCameraPhotoAsMediaResult(photo), + () => this.takePhotoCameraInputExperience(options, resolve, reject), + resolve, + reject + ); } private takePhotoCameraInputExperience(options: TakePhotoOptions, resolve: any, reject: any) { - let input = document.querySelector('#_capacitor-camera-input-takephoto') as HTMLInputElement; + const input = this._createFileInput('_capacitor-camera-input-takephoto'); const cleanup = () => { input.parentNode?.removeChild(input); }; - if (!input) { - input = document.createElement('input') as HTMLInputElement; - input.id = '_capacitor-camera-input-takephoto'; - input.type = 'file'; - input.hidden = true; - document.body.appendChild(input); + if (!input.onchange) { input.addEventListener('change', async (_e: any) => { - if (!input.files || input.files.length === 0) { - reject(new CapacitorException('No file selected')); - cleanup(); + if (!this._validateFileInput(input, reject, cleanup)) { return; } - const file = input.files[0]; - let format = 'jpeg'; - - if (file.type === 'image/png') { - format = 'png'; - } else if (file.type === 'image/gif') { - format = 'gif'; - } - - // Get resolution from image file + const file = input.files![0]; + const format = this._getFileFormat(file); const resolution = await this._getImageResolution(file); const reader = new FileReader(); @@ -364,10 +301,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { reader.readAsDataURL(file); }); - input.addEventListener('cancel', (_e: any) => { - reject(new CapacitorException('User cancelled photos app')); - cleanup(); - }); + this._setupInputCancelListener(input, reject, cleanup); } input.accept = 'image/*'; @@ -382,46 +316,30 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { } private galleryInputExperience(options: ChooseFromGalleryOptions, resolve: any, reject: any) { - let input = document.querySelector('#_capacitor-camera-input-gallery') as HTMLInputElement; + const input = this._createFileInput('_capacitor-camera-input-gallery'); + input.multiple = options.allowMultipleSelection ?? false; const cleanup = () => { input.parentNode?.removeChild(input); }; - if (!input) { - input = document.createElement('input') as HTMLInputElement; - input.id = '_capacitor-camera-input-gallery'; - input.type = 'file'; - input.hidden = true; - input.multiple = options.allowMultipleSelection ?? false; - document.body.appendChild(input); + if (!input.onchange) { input.addEventListener('change', async (_e: any) => { - if (!input.files || input.files.length === 0) { - reject(new CapacitorException('No files selected')); - cleanup(); + if (!this._validateFileInput(input, reject, cleanup)) { return; } const results: MediaResult[] = []; // eslint-disable-next-line @typescript-eslint/prefer-for-of - for (let i = 0; i < input.files.length; i++) { - const file = input.files[i]; - let format = 'jpeg'; + for (let i = 0; i < input.files!.length; i++) { + const file = input.files![i]; + const format = this._getFileFormat(file); let type = MediaType.Photo; let resolution = '0x0'; if (file.type.startsWith('image/')) { - if (file.type === 'image/png') { - format = 'png'; - } else if (file.type === 'image/gif') { - format = 'gif'; - } - - // Get resolution from image file resolution = await this._getImageResolution(file); - - // Get base64 thumbnail for image const thumbnail = await this._getBase64FromFile(file); results.push({ @@ -438,9 +356,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { }); } else if (file.type.startsWith('video/')) { type = MediaType.Video; - format = file.type.split('/')[1]; - // Get resolution, duration, and thumbnail from video file let duration: number | undefined; let thumbnail: string | undefined; try { @@ -470,10 +386,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { resolve({ results }); cleanup(); }); - input.addEventListener('cancel', (_e: any) => { - reject(new CapacitorException('User cancelled photos app')); - cleanup(); - }); + this._setupInputCancelListener(input, reject, cleanup); } // Set accept attribute based on mediaType @@ -493,9 +406,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { private async _getCameraPhotoAsMediaResult(photo: Blob): Promise { return new Promise(async (resolve, reject) => { const reader = new FileReader(); - const format = photo.type.split('/')[1]; - - // Get resolution from image blob + const format = this._getFileFormat(photo); const resolution = await this._getImageResolution(photo); reader.readAsDataURL(photo); @@ -521,6 +432,90 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { }); } + private _getFileFormat(file: File | Blob): string { + if (file.type === 'image/png') { + return 'png'; + } else if (file.type === 'image/gif') { + return 'gif'; + } else if (file.type.startsWith('video/')) { + return file.type.split('/')[1]; + } else if (file.type.startsWith('image/')) { + return 'jpeg'; + } + return file.type.split('/')[1] || 'jpeg'; + } + + private _validateFileInput(input: HTMLInputElement, reject: any, cleanup: () => void): boolean { + if (!input.files || input.files.length === 0) { + const message = input.multiple ? 'No files selected' : 'No file selected'; + reject(new CapacitorException(message)); + cleanup(); + return false; + } + return true; + } + + private async _setupPWACameraModal( + cameraDirection: CameraDirection | undefined, + onPhotoCallback: (photo: Blob) => Promise, + fallbackCallback: () => void, + resolve: any, + reject: any + ): Promise { + if (customElements.get('pwa-camera-modal')) { + const cameraModal: any = document.createElement('pwa-camera-modal'); + cameraModal.facingMode = cameraDirection === CameraDirection.Front ? 'user' : 'environment'; + document.body.appendChild(cameraModal); + try { + await cameraModal.componentOnReady(); + cameraModal.addEventListener('onPhoto', async (e: any) => { + const photo = e.detail; + + if (photo === null) { + reject(new CapacitorException('User cancelled photos app')); + } else if (photo instanceof Error) { + reject(photo); + } else { + resolve(await onPhotoCallback(photo)); + } + + cameraModal.dismiss(); + document.body.removeChild(cameraModal); + }); + + cameraModal.present(); + } catch (e) { + fallbackCallback(); + } + } else { + console.error( + `Unable to load PWA Element 'pwa-camera-modal'. See the docs: https://capacitorjs.com/docs/web/pwa-elements.`, + ); + fallbackCallback(); + } + } + + private _createFileInput(id: string): HTMLInputElement { + let input = document.querySelector(`#${id}`) as HTMLInputElement; + + if (!input) { + input = document.createElement('input') as HTMLInputElement; + input.id = id; + input.type = 'file'; + input.hidden = true; + document.body.appendChild(input); + } + + return input; + } + + private _setupInputCancelListener(input: HTMLInputElement, reject: any, cleanup: () => void): void { + input.addEventListener('cancel', (_e: any) => { + reject(new CapacitorException('User cancelled photos app')); + cleanup(); + }); + } + private async _getImageResolution(image: Blob | File): Promise { try { const bitmap = await createImageBitmap(image); From 5463939be1e01ff356520940d23a5b7dfe366a18 Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Tue, 24 Mar 2026 10:33:33 -0400 Subject: [PATCH 09/26] feat: use includeMetadata for takePhoto and chooseFromGallery on Web --- README.md | 14 ++++---- src/definitions.ts | 3 -- src/web.ts | 89 +++++++++++++++++++++++++++++----------------- 3 files changed, 64 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 42ef57e..beefa52 100644 --- a/README.md +++ b/README.md @@ -386,7 +386,7 @@ Allows the user to pick multiple pictures from the photo gallery. | **`editInApp`** | boolean | If `true`, will use an in-app editor for photo edition. If `false`, will open a separate (platform-specific) native app to handle photo edition, falling back to the in-app editor if none is available. Only applicable with `allowEdit` set to true. Note: This option is only supported on Android and iOS. | true | 8.1.0 | | **`presentationStyle`** | 'fullscreen' \| 'popover' | iOS only: The presentation style of the Camera. | 'fullscreen' | 8.1.0 | | **`webUseInput`** | boolean | Web only: Whether to use the PWA Element experience or file input. The default is to use PWA Elements if installed and fall back to file input. To always use file input, set this to `true`. Learn more about PWA Elements: https://capacitorjs.com/docs/web/pwa-elements | | 8.1.0 | -| **`includeMetadata`** | boolean | Whether or not MediaResult should include its metadata. If an error occurs when obtaining the metadata, it will return empty. Note: This option is only supported on Android and iOS. | false | 8.1.0 | +| **`includeMetadata`** | boolean | Whether or not MediaResult should include its metadata. If an error occurs when obtaining the metadata, it will return empty. | false | 8.1.0 | #### RecordVideoOptions @@ -419,7 +419,7 @@ Allows the user to pick multiple pictures from the photo gallery. | **`mediaType`** | MediaTypeSelection | The type of media to select. Can be pictures, videos, or both. | MediaTypeSelection.Photo | 8.1.0 | | **`allowMultipleSelection`** | boolean | Whether or not to allow selecting multiple media files from the gallery. | false | 8.1.0 | | **`limit`** | number | The maximum number of media files that the user can choose. Only applicable if `allowMultipleSelection` is `true`. Any non-positive number will be treated as unlimited. Note: This option is only supported on Android 13+ and iOS. Not available on Web. | 0 | 8.1.0 | -| **`includeMetadata`** | boolean | Whether or not MediaResult should include its metadata. If an error occurs when obtaining the metadata, it will return empty. Note: This option is only supported on Android and iOS. | false | 8.1.0 | +| **`includeMetadata`** | boolean | Whether or not MediaResult should include its metadata. If an error occurs when obtaining the metadata, it will return empty. | false | 8.1.0 | | **`allowEdit`** | boolean | Whether to allow the user to crop or make small edits. Only applicable for `MediaTypeSelection.Photo` and `allowMultipleSelection` set to `false`. Note: This option is only supported on Android and iOS. | false | 8.1.0 | | **`editInApp`** | boolean | If `true`, will use an in-app editor for photo edition. If `false`, will open a separate (platform-specific) native app to handle photo edition, falling back to the in-app editor if none is available. Only applicable with `allowEdit` set to true. Note: This option is only supported on Android and iOS. | true | 8.1.0 | | **`presentationStyle`** | 'fullscreen' \| 'popover' | iOS only: The presentation style of media picker. | 'fullscreen' | 8.1.0 | @@ -446,11 +446,11 @@ Allows the user to pick multiple pictures from the photo gallery. #### EditURIPhotoOptions -| Prop | Type | Description | Default | Since | -| --------------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ----- | -| **`uri`** | string | The URI that contains the photo to edit. | | 8.1.0 | -| **`saveToGallery`** | boolean | Whether to save the edited photo to the gallery. | false | 8.1.0 | -| **`includeMetadata`** | boolean | Whether or not MediaResult should include its metadata. If an error occurs when obtaining the metadata, it will return empty. Note: This option is only supported on Android and iOS. | false | 8.1.0 | +| Prop | Type | Description | Default | Since | +| --------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ----- | +| **`uri`** | string | The URI that contains the photo to edit. | | 8.1.0 | +| **`saveToGallery`** | boolean | Whether to save the edited photo to the gallery. | false | 8.1.0 | +| **`includeMetadata`** | boolean | Whether or not MediaResult should include its metadata. If an error occurs when obtaining the metadata, it will return empty. | false | 8.1.0 | #### GalleryPhotos diff --git a/src/definitions.ts b/src/definitions.ts index fddd998..b335c10 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -214,7 +214,6 @@ export interface TakePhotoOptions { /** * Whether or not MediaResult should include its metadata. * If an error occurs when obtaining the metadata, it will return empty. - * Note: This option is only supported on Android and iOS. * @default false * * @since 8.1.0 @@ -292,7 +291,6 @@ export interface ChooseFromGalleryOptions { /** * Whether or not MediaResult should include its metadata. * If an error occurs when obtaining the metadata, it will return empty. - * Note: This option is only supported on Android and iOS. * @default false * * @since 8.1.0 @@ -401,7 +399,6 @@ export interface EditURIPhotoOptions { /** * Whether or not MediaResult should include its metadata. * If an error occurs when obtaining the metadata, it will return empty. - * Note: This option is only supported on Android and iOS. * @default false * * @since 8.1.0 diff --git a/src/web.ts b/src/web.ts index 7f3df78..f2b7279 100644 --- a/src/web.ts +++ b/src/web.ts @@ -257,7 +257,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { private async takePhotoCameraExperience(options: TakePhotoOptions, resolve: any, reject: any) { await this._setupPWACameraModal( options.cameraDirection, - (photo) => this._getCameraPhotoAsMediaResult(photo), + (photo) => this._getCameraPhotoAsMediaResult(photo, options.includeMetadata ?? false), () => this.takePhotoCameraInputExperience(options, resolve, reject), resolve, reject @@ -279,23 +279,29 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { const file = input.files![0]; const format = this._getFileFormat(file); - const resolution = await this._getImageResolution(file); const reader = new FileReader(); - reader.addEventListener('load', () => { + reader.addEventListener('load', async () => { const b64 = (reader.result as string).split(',')[1]; - resolve({ + + const result: MediaResult = { type: MediaType.Photo, thumbnail: b64, webPath: URL.createObjectURL(file), saved: false, - metadata: { + }; + + if (options.includeMetadata) { + const resolution = await this._getImageResolution(file); + result.metadata = { format, resolution, size: file.size, creationDate: new Date(file.lastModified).toISOString(), - }, - } as MediaResult); + }; + } + + resolve(result); cleanup(); }); @@ -336,51 +342,64 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { const file = input.files![i]; const format = this._getFileFormat(file); let type = MediaType.Photo; - let resolution = '0x0'; if (file.type.startsWith('image/')) { - resolution = await this._getImageResolution(file); const thumbnail = await this._getBase64FromFile(file); - results.push({ + const result: MediaResult = { type, thumbnail, webPath: URL.createObjectURL(file), saved: false, - metadata: { + }; + + if (options.includeMetadata) { + const resolution = await this._getImageResolution(file); + result.metadata = { format, resolution, size: file.size, creationDate: new Date(file.lastModified).toISOString(), - }, - }); + }; + } + + results.push(result); } else if (file.type.startsWith('video/')) { type = MediaType.Video; - let duration: number | undefined; let thumbnail: string | undefined; - try { - const videoMetadata = await this._getVideoMetadata(file); - resolution = videoMetadata.resolution; - duration = videoMetadata.duration; - thumbnail = videoMetadata.thumbnail; - } catch (e) { - console.warn('Failed to get video metadata:', e); + let resolution: string | undefined; + let duration: number | undefined; + + if (options.includeMetadata) { + try { + const videoMetadata = await this._getVideoMetadata(file); + resolution = videoMetadata.resolution; + duration = videoMetadata.duration; + thumbnail = videoMetadata.thumbnail; + } catch (e) { + console.warn('Failed to get video metadata:', e); + } } - results.push({ + const result: MediaResult = { type, thumbnail, webPath: URL.createObjectURL(file), saved: false, - metadata: { + }; + + if (options.includeMetadata && resolution) { + result.metadata = { format, resolution, size: file.size, creationDate: new Date(file.lastModified).toISOString(), duration, - }, - }); + }; + } + + results.push(result); } } resolve({ results }); @@ -403,28 +422,34 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { input.click(); } - private async _getCameraPhotoAsMediaResult(photo: Blob): Promise { + private async _getCameraPhotoAsMediaResult(photo: Blob, includeMetadata: boolean): Promise { return new Promise(async (resolve, reject) => { const reader = new FileReader(); const format = this._getFileFormat(photo); - const resolution = await this._getImageResolution(photo); reader.readAsDataURL(photo); - reader.onloadend = () => { + reader.onloadend = async () => { const r = reader.result as string; const b64 = r.split(',')[1]; - resolve({ + + const result: MediaResult = { type: MediaType.Photo, thumbnail: b64, webPath: URL.createObjectURL(photo), saved: false, - metadata: { + }; + + if (includeMetadata) { + const resolution = await this._getImageResolution(photo); + result.metadata = { format, resolution, size: photo.size, creationDate: new Date().toISOString(), - }, - }); + }; + } + + resolve(result); }; reader.onerror = (e) => { reject(e); From db31e98c38ae56c40d080e10b5ec1b7a7c282819 Mon Sep 17 00:00:00 2001 From: Rui Mendes Date: Tue, 24 Mar 2026 16:09:01 +0000 Subject: [PATCH 10/26] fix: show photos on older methods when resultType base64 or dataUrl --- .../camera/old-methods/GetPhotoConfigurable.tsx | 4 ++++ .../src/components/camera/old-methods/types.ts | 2 ++ example-app/src/pages/TakePicturePage.tsx | 12 ++++++++++-- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/example-app/src/components/camera/old-methods/GetPhotoConfigurable.tsx b/example-app/src/components/camera/old-methods/GetPhotoConfigurable.tsx index 70ec169..3665f08 100644 --- a/example-app/src/components/camera/old-methods/GetPhotoConfigurable.tsx +++ b/example-app/src/components/camera/old-methods/GetPhotoConfigurable.tsx @@ -72,6 +72,8 @@ class GetPhotoConfigurable extends React.Component< this.props.onPhotoResult({ path: photo.path, webPath: photo.webPath, + base64String: photo.base64String, + dataUrl: photo.dataUrl, exif: photo.exif, }); } catch (e) { @@ -105,6 +107,8 @@ class GetPhotoConfigurable extends React.Component< this.props.onPhotoResult({ path: photo.path, webPath: photo.webPath, + base64String: photo.base64String, + dataUrl: photo.dataUrl, exif: photo.exif, }); } catch (e) { diff --git a/example-app/src/components/camera/old-methods/types.ts b/example-app/src/components/camera/old-methods/types.ts index cddd3b0..d77251d 100644 --- a/example-app/src/components/camera/old-methods/types.ts +++ b/example-app/src/components/camera/old-methods/types.ts @@ -30,5 +30,7 @@ export interface PickImagesConfig { export interface PhotoResult { path?: string; webPath?: string; + base64String?: string; + dataUrl?: string; exif?: any; } diff --git a/example-app/src/pages/TakePicturePage.tsx b/example-app/src/pages/TakePicturePage.tsx index 6411e55..cddaa21 100644 --- a/example-app/src/pages/TakePicturePage.tsx +++ b/example-app/src/pages/TakePicturePage.tsx @@ -58,14 +58,22 @@ class TakePicturePage extends React.Component<{}, ITakePicturePageState> { handlePhotoResult = (result: { path?: string; webPath?: string; + base64String?: string; + dataUrl?: string; exif?: any; }): void => { + const filePath = + result.path ?? + result.webPath ?? + result.dataUrl ?? + (result.base64String ? `data:image/jpeg;base64,${result.base64String}` : null); + this.setState({ - filePath: result.path ?? result.webPath ?? null, + filePath, metadata: JSON.stringify(result.exif, null, 2), }); - if (result.path || result.webPath) { + if (filePath) { MediaHistoryService.addMedia({ mediaType: "photo", method: "getPhoto", From 8b0f1c5f30ffeb4568b1101e374b466532a1ed0d Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Tue, 24 Mar 2026 15:25:39 -0400 Subject: [PATCH 11/26] refactor: avoid code duplication and improve event listeners --- src/web.ts | 234 ++++++++++++++++++++--------------------------------- 1 file changed, 86 insertions(+), 148 deletions(-) diff --git a/src/web.ts b/src/web.ts index f2b7279..9eb95b9 100644 --- a/src/web.ts +++ b/src/web.ts @@ -257,7 +257,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { private async takePhotoCameraExperience(options: TakePhotoOptions, resolve: any, reject: any) { await this._setupPWACameraModal( options.cameraDirection, - (photo) => this._getCameraPhotoAsMediaResult(photo, options.includeMetadata ?? false), + (photo) => this._buildPhotoMediaResult(photo, options.includeMetadata ?? false), () => this.takePhotoCameraInputExperience(options, resolve, reject), resolve, reject @@ -271,44 +271,20 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { input.parentNode?.removeChild(input); }; - if (!input.onchange) { - input.addEventListener('change', async (_e: any) => { - if (!this._validateFileInput(input, reject, cleanup)) { - return; - } - - const file = input.files![0]; - const format = this._getFileFormat(file); - - const reader = new FileReader(); - reader.addEventListener('load', async () => { - const b64 = (reader.result as string).split(',')[1]; - - const result: MediaResult = { - type: MediaType.Photo, - thumbnail: b64, - webPath: URL.createObjectURL(file), - saved: false, - }; - - if (options.includeMetadata) { - const resolution = await this._getImageResolution(file); - result.metadata = { - format, - resolution, - size: file.size, - creationDate: new Date(file.lastModified).toISOString(), - }; - } + input.onchange = async (_e: any) => { + if (!this._validateFileInput(input, reject, cleanup)) { + return; + } - resolve(result); - cleanup(); - }); + const file = input.files![0]; + resolve(await this._buildPhotoMediaResult(file, options.includeMetadata ?? false)); + cleanup(); + }; - reader.readAsDataURL(file); - }); - this._setupInputCancelListener(input, reject, cleanup); - } + input.oncancel = () => { + reject(new CapacitorException('User cancelled photos app')); + cleanup(); + }; input.accept = 'image/*'; if (options.cameraDirection === CameraDirection.Front) { @@ -329,84 +305,64 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { input.parentNode?.removeChild(input); }; - if (!input.onchange) { - input.addEventListener('change', async (_e: any) => { - if (!this._validateFileInput(input, reject, cleanup)) { - return; - } + input.onchange = async (_e: any) => { + if (!this._validateFileInput(input, reject, cleanup)) { + return; + } - const results: MediaResult[] = []; + const results: MediaResult[] = []; - // eslint-disable-next-line @typescript-eslint/prefer-for-of - for (let i = 0; i < input.files!.length; i++) { - const file = input.files![i]; - const format = this._getFileFormat(file); - let type = MediaType.Photo; - - if (file.type.startsWith('image/')) { - const thumbnail = await this._getBase64FromFile(file); + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let i = 0; i < input.files!.length; i++) { + const file = input.files![i]; - const result: MediaResult = { - type, - thumbnail, - webPath: URL.createObjectURL(file), - saved: false, - }; + if (file.type.startsWith('image/')) { + results.push(await this._buildPhotoMediaResult(file, options.includeMetadata ?? false)); + } else if (file.type.startsWith('video/')) { + const format = this._getFileFormat(file); + let thumbnail: string | undefined; + let resolution: string | undefined; + let duration: number | undefined; - if (options.includeMetadata) { - const resolution = await this._getImageResolution(file); - result.metadata = { - format, - resolution, - size: file.size, - creationDate: new Date(file.lastModified).toISOString(), - }; + if (options.includeMetadata) { + try { + const videoMetadata = await this._getVideoMetadata(file); + resolution = videoMetadata.resolution; + duration = videoMetadata.duration; + thumbnail = videoMetadata.thumbnail; + } catch (e) { + console.warn('Failed to get video metadata:', e); } + } - results.push(result); - } else if (file.type.startsWith('video/')) { - type = MediaType.Video; - - let thumbnail: string | undefined; - let resolution: string | undefined; - let duration: number | undefined; - - if (options.includeMetadata) { - try { - const videoMetadata = await this._getVideoMetadata(file); - resolution = videoMetadata.resolution; - duration = videoMetadata.duration; - thumbnail = videoMetadata.thumbnail; - } catch (e) { - console.warn('Failed to get video metadata:', e); - } - } + const result: MediaResult = { + type: MediaType.Video, + thumbnail, + webPath: URL.createObjectURL(file), + saved: false, + }; - const result: MediaResult = { - type, - thumbnail, - webPath: URL.createObjectURL(file), - saved: false, + if (options.includeMetadata && resolution) { + result.metadata = { + format, + resolution, + size: file.size, + creationDate: new Date(file.lastModified).toISOString(), + duration, }; - - if (options.includeMetadata && resolution) { - result.metadata = { - format, - resolution, - size: file.size, - creationDate: new Date(file.lastModified).toISOString(), - duration, - }; - } - - results.push(result); } + + results.push(result); } - resolve({ results }); - cleanup(); - }); - this._setupInputCancelListener(input, reject, cleanup); - } + } + resolve({ results }); + cleanup(); + }; + + input.oncancel = () => { + reject(new CapacitorException('User cancelled photos app')); + cleanup(); + }; // Set accept attribute based on mediaType const mediaType = options.mediaType ?? MediaTypeSelection.Photo; @@ -422,41 +378,6 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { input.click(); } - private async _getCameraPhotoAsMediaResult(photo: Blob, includeMetadata: boolean): Promise { - return new Promise(async (resolve, reject) => { - const reader = new FileReader(); - const format = this._getFileFormat(photo); - - reader.readAsDataURL(photo); - reader.onloadend = async () => { - const r = reader.result as string; - const b64 = r.split(',')[1]; - - const result: MediaResult = { - type: MediaType.Photo, - thumbnail: b64, - webPath: URL.createObjectURL(photo), - saved: false, - }; - - if (includeMetadata) { - const resolution = await this._getImageResolution(photo); - result.metadata = { - format, - resolution, - size: photo.size, - creationDate: new Date().toISOString(), - }; - } - - resolve(result); - }; - reader.onerror = (e) => { - reject(e); - }; - }); - } - private _getFileFormat(file: File | Blob): string { if (file.type === 'image/png') { return 'png'; @@ -470,6 +391,30 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { return file.type.split('/')[1] || 'jpeg'; } + private async _buildPhotoMediaResult(file: File | Blob, includeMetadata: boolean): Promise { + const format = this._getFileFormat(file); + const thumbnail = await this._getBase64FromFile(file); + + const result: MediaResult = { + type: MediaType.Photo, + thumbnail, + webPath: URL.createObjectURL(file), + saved: false, + }; + + if (includeMetadata) { + const resolution = await this._getImageResolution(file); + result.metadata = { + format, + resolution, + size: file.size, + creationDate: 'lastModified' in file ? new Date(file.lastModified).toISOString() : new Date().toISOString(), + }; + } + + return result; + } + private _validateFileInput(input: HTMLInputElement, reject: any, cleanup: () => void): boolean { if (!input.files || input.files.length === 0) { const message = input.multiple ? 'No files selected' : 'No file selected'; @@ -534,13 +479,6 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { return input; } - private _setupInputCancelListener(input: HTMLInputElement, reject: any, cleanup: () => void): void { - input.addEventListener('cancel', (_e: any) => { - reject(new CapacitorException('User cancelled photos app')); - cleanup(); - }); - } - private async _getImageResolution(image: Blob | File): Promise { try { const bitmap = await createImageBitmap(image); @@ -553,7 +491,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { } } - private _getBase64FromFile(file: File): Promise { + private _getBase64FromFile(file: File | Blob): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => { From 0f4c9f3e2678312b432db01bf712162ce6aea7dd Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Tue, 24 Mar 2026 17:39:42 -0400 Subject: [PATCH 12/26] chore(android): update Android bridge with new lib version --- .../main/java/com/capacitorjs/plugins/camera/IonCameraFlow.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/android/src/main/java/com/capacitorjs/plugins/camera/IonCameraFlow.kt b/android/src/main/java/com/capacitorjs/plugins/camera/IonCameraFlow.kt index 8bcc33e..c46305f 100644 --- a/android/src/main/java/com/capacitorjs/plugins/camera/IonCameraFlow.kt +++ b/android/src/main/java/com/capacitorjs/plugins/camera/IonCameraFlow.kt @@ -832,9 +832,6 @@ class IonCameraFlow( activity, intent, ionParams, - { image -> - //TODO remove this callback - }, { mediaResult -> handleMediaResult(mediaResult) }, @@ -938,7 +935,6 @@ class IonCameraFlow( correctOrientation = correctOrientation, saveToPhotoAlbum = saveToGallery, includeMetadata = includeMetadata, - latestVersion = true //TODO check this, because now we don't have resultType in the new Api ) } From d0d2a6f88291f4a736f7602cc5bbba7f47e46166 Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Wed, 25 Mar 2026 08:57:14 -0400 Subject: [PATCH 13/26] chore: remove redundant doc --- src/definitions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/definitions.ts b/src/definitions.ts index b335c10..30e02ad 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -281,7 +281,7 @@ export interface ChooseFromGalleryOptions { * The maximum number of media files that the user can choose. * Only applicable if `allowMultipleSelection` is `true`. * Any non-positive number will be treated as unlimited. - * Note: This option is only supported on Android 13+ and iOS. Not available on Web. + * Note: This option is only supported on Android 13+ and iOS. * @default 0 * * @since 8.1.0 From 6573fecb1dcdd2ed60ddc6ee99fd026d86cdc244 Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Wed, 25 Mar 2026 09:15:58 -0400 Subject: [PATCH 14/26] chore: update docs --- README.md | 18 +++++++++--------- src/definitions.ts | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index beefa52..83d2982 100644 --- a/README.md +++ b/README.md @@ -349,14 +349,14 @@ Allows the user to pick multiple pictures from the photo gallery. #### MediaResult -| Prop | Type | Description | Since | -| --------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | -| **`type`** | MediaType | The type of media result. Either `Photo` or `Video`. | 8.1.0 | -| **`uri`** | string | The URI pointing to the media file. Not available on Web. Use `webPath` instead for Web. | 8.1.0 | -| **`thumbnail`** | string | Returns the thumbnail of the media, base64 encoded. On Web, for `MediaType.Photo`, the full image is returned here, also base64 encoded. On Web, for `MediaType.Video`, a JPEG thumbnail captured from the video is returned. | 8.1.0 | -| **`saved`** | boolean | Whether if the media was saved to the gallery successfully or not. Only applicable if `saveToGallery` was set to `true` in input options. Otherwise, `false` is always returned for `save`. Not available on Web. | 8.1.0 | -| **`webPath`** | string | webPath returns a path that can be used to set the src attribute of a media item for efficient loading and rendering. | 8.1.0 | -| **`metadata`** | MediaMetadata | Metadata associated to the media result. Only included if `includeMetadata` was set to `true` in input options. | 8.1.0 | +| Prop | Type | Description | Since | +| --------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | +| **`type`** | MediaType | The type of media result. Either `Photo` or `Video`. | 8.1.0 | +| **`uri`** | string | The URI pointing to the media file. Not available on Web. Use `webPath` instead for Web. | 8.1.0 | +| **`thumbnail`** | string | Returns the thumbnail of the media, base64 encoded. On Web, for `MediaType.Photo`, the full image is returned here, also base64 encoded. On Web, for `MediaType.Video`, a full-resolution JPEG frame captured from the video is returned, base64 encoded at 80% quality. | 8.1.0 | +| **`saved`** | boolean | Whether if the media was saved to the gallery successfully or not. Only applicable if `saveToGallery` was set to `true` in input options. Otherwise, `false` is always returned for `save`. Not available on Web. | 8.1.0 | +| **`webPath`** | string | webPath returns a path that can be used to set the src attribute of a media item for efficient loading and rendering. | 8.1.0 | +| **`metadata`** | MediaMetadata | Metadata associated to the media result. Only included if `includeMetadata` was set to `true` in input options. | 8.1.0 | #### MediaMetadata @@ -418,7 +418,7 @@ Allows the user to pick multiple pictures from the photo gallery. | ---------------------------- | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | ----- | | **`mediaType`** | MediaTypeSelection | The type of media to select. Can be pictures, videos, or both. | MediaTypeSelection.Photo | 8.1.0 | | **`allowMultipleSelection`** | boolean | Whether or not to allow selecting multiple media files from the gallery. | false | 8.1.0 | -| **`limit`** | number | The maximum number of media files that the user can choose. Only applicable if `allowMultipleSelection` is `true`. Any non-positive number will be treated as unlimited. Note: This option is only supported on Android 13+ and iOS. Not available on Web. | 0 | 8.1.0 | +| **`limit`** | number | The maximum number of media files that the user can choose. Only applicable if `allowMultipleSelection` is `true`. Any non-positive number will be treated as unlimited. Note: This option is only supported on Android 13+ and iOS. | 0 | 8.1.0 | | **`includeMetadata`** | boolean | Whether or not MediaResult should include its metadata. If an error occurs when obtaining the metadata, it will return empty. | false | 8.1.0 | | **`allowEdit`** | boolean | Whether to allow the user to crop or make small edits. Only applicable for `MediaTypeSelection.Photo` and `allowMultipleSelection` set to `false`. Note: This option is only supported on Android and iOS. | false | 8.1.0 | | **`editInApp`** | boolean | If `true`, will use an in-app editor for photo edition. If `false`, will open a separate (platform-specific) native app to handle photo edition, falling back to the in-app editor if none is available. Only applicable with `allowEdit` set to true. Note: This option is only supported on Android and iOS. | true | 8.1.0 | diff --git a/src/definitions.ts b/src/definitions.ts index 30e02ad..1b4b946 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -443,7 +443,7 @@ export interface MediaResult { /** * Returns the thumbnail of the media, base64 encoded. * On Web, for `MediaType.Photo`, the full image is returned here, also base64 encoded. - * On Web, for `MediaType.Video`, a JPEG thumbnail captured from the video is returned. + * On Web, for `MediaType.Video`, a full-resolution JPEG frame captured from the video is returned, base64 encoded at 80% quality. * * @since 8.1.0 */ From 2ca861734553db0f14ce69194315be4e3bc26524 Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Wed, 25 Mar 2026 09:17:31 -0400 Subject: [PATCH 15/26] chore: update docs --- README.md | 2 +- src/definitions.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 83d2982..b3a9bee 100644 --- a/README.md +++ b/README.md @@ -367,7 +367,7 @@ Allows the user to pick multiple pictures from the photo gallery. | **`duration`** | number | Only applicable for `MediaType.Video` - the duration of the media, in seconds. | 8.1.0 | | **`format`** | string | The format of the image, ex: jpeg, png, mp4. Web supports jpeg, png and gif, but the exact availability may vary depending on the browser. gif is only supported for `chooseFromGallery`, and only if `webUseInput` option is set to `true`. | 8.1.0 | | **`resolution`** | string | The resolution of the media, in `<width>x<height>` format. Example: '1920x1080'. | 8.1.0 | -| **`creationDate`** | string | The date and time the media was created, in ISO 8601 format. If creation date is not available (e.g. Android 7 and below), the last modified date is returned. | 8.1.0 | +| **`creationDate`** | string | The date and time the media was created, in ISO 8601 format. If creation date is not available (e.g. Android 7 and below), the last modified date is returned. For Web, the last modified date is always returned. | 8.1.0 | | **`exif`** | string | Exif data, if any, retreived from the media item. Not available on Web. | 8.1.0 | diff --git a/src/definitions.ts b/src/definitions.ts index 1b4b946..e420b2c 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -511,6 +511,7 @@ export interface MediaMetadata { /** * The date and time the media was created, in ISO 8601 format. * If creation date is not available (e.g. Android 7 and below), the last modified date is returned. + * For Web, the last modified date is always returned. * * @since 8.1.0 */ From aeaadff02d7245c0c96c31bb2cf9c138316885be Mon Sep 17 00:00:00 2001 From: "Alex J." Date: Wed, 25 Mar 2026 09:18:37 -0400 Subject: [PATCH 16/26] chore: update src/definitions.ts Co-authored-by: Pedro Bilro --- src/definitions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/definitions.ts b/src/definitions.ts index e420b2c..e6524e2 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -495,7 +495,7 @@ export interface MediaMetadata { * The format of the image, ex: jpeg, png, mp4. * * Web supports jpeg, png and gif, but the exact availability may vary depending on the browser. - * gif is only supported for `chooseFromGallery`, and only if `webUseInput` option is set to `true`. + * gif is only supported for `chooseFromGallery` on Web. * * @since 8.1.0 */ From 8998b2c5ef02b6771e72a5ba8d051e014a792559 Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Wed, 25 Mar 2026 09:28:10 -0400 Subject: [PATCH 17/26] refactor: make Photo.saved optional like other parameters (e.g. exif) --- README.md | 16 ++++++++-------- src/definitions.ts | 2 +- src/web.ts | 3 --- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index b3a9bee..b3a4c1f 100644 --- a/README.md +++ b/README.md @@ -361,14 +361,14 @@ Allows the user to pick multiple pictures from the photo gallery. #### MediaMetadata -| Prop | Type | Description | Since | -| ------------------ | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | -| **`size`** | number | File size of the media, in bytes. | 8.1.0 | -| **`duration`** | number | Only applicable for `MediaType.Video` - the duration of the media, in seconds. | 8.1.0 | -| **`format`** | string | The format of the image, ex: jpeg, png, mp4. Web supports jpeg, png and gif, but the exact availability may vary depending on the browser. gif is only supported for `chooseFromGallery`, and only if `webUseInput` option is set to `true`. | 8.1.0 | -| **`resolution`** | string | The resolution of the media, in `<width>x<height>` format. Example: '1920x1080'. | 8.1.0 | -| **`creationDate`** | string | The date and time the media was created, in ISO 8601 format. If creation date is not available (e.g. Android 7 and below), the last modified date is returned. For Web, the last modified date is always returned. | 8.1.0 | -| **`exif`** | string | Exif data, if any, retreived from the media item. Not available on Web. | 8.1.0 | +| Prop | Type | Description | Since | +| ------------------ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----- | +| **`size`** | number | File size of the media, in bytes. | 8.1.0 | +| **`duration`** | number | Only applicable for `MediaType.Video` - the duration of the media, in seconds. | 8.1.0 | +| **`format`** | string | The format of the image, ex: jpeg, png, mp4. Web supports jpeg, png and gif, but the exact availability may vary depending on the browser. gif is only supported for `chooseFromGallery` on Web. | 8.1.0 | +| **`resolution`** | string | The resolution of the media, in `<width>x<height>` format. Example: '1920x1080'. | 8.1.0 | +| **`creationDate`** | string | The date and time the media was created, in ISO 8601 format. If creation date is not available (e.g. Android 7 and below), the last modified date is returned. For Web, the last modified date is always returned. | 8.1.0 | +| **`exif`** | string | Exif data, if any, retreived from the media item. Not available on Web. | 8.1.0 | #### TakePhotoOptions diff --git a/src/definitions.ts b/src/definitions.ts index e6524e2..4f66437 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -722,7 +722,7 @@ export interface Photo { * * @since 1.1.0 */ - saved: boolean; + saved?: boolean; } export interface GalleryPhotos { diff --git a/src/web.ts b/src/web.ts index 9eb95b9..c7a25b2 100644 --- a/src/web.ts +++ b/src/web.ts @@ -227,7 +227,6 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { resolve({ webPath: URL.createObjectURL(photo), format, - saved: false, }); } else { reader.readAsDataURL(photo); @@ -237,13 +236,11 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { resolve({ dataUrl: r, format, - saved: false, }); } else { resolve({ base64String: r.split(',')[1], format, - saved: false, }); } }; From 8e3f565695b96756bcb433e6f069846961494f68 Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Wed, 25 Mar 2026 09:43:36 -0400 Subject: [PATCH 18/26] refactor: make MediaMetadata.resolution optional --- src/definitions.ts | 2 +- src/web.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/definitions.ts b/src/definitions.ts index 4f66437..c3b189b 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -506,7 +506,7 @@ export interface MediaMetadata { * * @since 8.1.0 */ - resolution: string; + resolution?: string; /** * The date and time the media was created, in ISO 8601 format. diff --git a/src/web.ts b/src/web.ts index c7a25b2..929a48b 100644 --- a/src/web.ts +++ b/src/web.ts @@ -503,7 +503,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { }); } - private _getVideoMetadata(videoFile: File): Promise<{ resolution: string; duration: number; thumbnail?: string }> { + private _getVideoMetadata(videoFile: File): Promise<{ resolution?: string; duration?: number; thumbnail?: string }> { return new Promise((resolve) => { const video = document.createElement('video'); video.preload = 'metadata'; @@ -556,7 +556,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { video.onerror = () => { // Clean up and return defaults URL.revokeObjectURL(video.src); - resolve({ resolution: '0x0', duration: 0 }); + resolve({}); }; video.src = URL.createObjectURL(videoFile); From 92ede100c2967f19e42d88e439062c9ea36e6215 Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Wed, 25 Mar 2026 09:57:38 -0400 Subject: [PATCH 19/26] refactor: remove duplicated code --- src/web.ts | 32 +++++++++----------------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/src/web.ts b/src/web.ts index 929a48b..32b014d 100644 --- a/src/web.ts +++ b/src/web.ts @@ -516,8 +516,12 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { }; video.onseeked = () => { + const result: { resolution?: string; duration?: number; thumbnail?: string } = { + resolution: `${video.videoWidth}x${video.videoHeight}`, + duration: video.duration, + }; + try { - // Create canvas and capture frame const canvas = document.createElement('canvas'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; @@ -525,32 +529,14 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { if (ctx) { ctx.drawImage(video, 0, 0, canvas.width, canvas.height); - const thumbnail = canvas.toDataURL('image/jpeg', 0.8).split(',')[1]; - - // Clean up - URL.revokeObjectURL(video.src); - resolve({ - resolution: `${video.videoWidth}x${video.videoHeight}`, - duration: video.duration, - thumbnail, - }); - } else { - // Clean up and return without thumbnail - URL.revokeObjectURL(video.src); - resolve({ - resolution: `${video.videoWidth}x${video.videoHeight}`, - duration: video.duration, - }); + result.thumbnail = canvas.toDataURL('image/jpeg', 0.8).split(',')[1]; } } catch (e) { console.warn('Failed to generate video thumbnail:', e); - // Clean up and return without thumbnail - URL.revokeObjectURL(video.src); - resolve({ - resolution: `${video.videoWidth}x${video.videoHeight}`, - duration: video.duration, - }); } + + URL.revokeObjectURL(video.src); + resolve(result); }; video.onerror = () => { From 6772ebc49c8583d507f134ec8bdb8d285b6e0e97 Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Wed, 25 Mar 2026 10:01:28 -0400 Subject: [PATCH 20/26] refactor: avoid returning 0 resolution --- src/web.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/web.ts b/src/web.ts index 32b014d..588e814 100644 --- a/src/web.ts +++ b/src/web.ts @@ -476,7 +476,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { return input; } - private async _getImageResolution(image: Blob | File): Promise { + private async _getImageResolution(image: Blob | File): Promise { try { const bitmap = await createImageBitmap(image); const resolution = `${bitmap.width}x${bitmap.height}`; @@ -484,7 +484,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { return resolution; } catch (e) { console.warn('Failed to get image resolution:', e); - return '0x0'; + return undefined; } } From 2e37fc904b6db974705886d5150204c6ba05b11e Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Wed, 25 Mar 2026 10:09:09 -0400 Subject: [PATCH 21/26] fix: always return video thumbnail --- src/web.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/web.ts b/src/web.ts index 588e814..d571a7e 100644 --- a/src/web.ts +++ b/src/web.ts @@ -321,15 +321,16 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { let resolution: string | undefined; let duration: number | undefined; - if (options.includeMetadata) { - try { - const videoMetadata = await this._getVideoMetadata(file); - resolution = videoMetadata.resolution; - duration = videoMetadata.duration; - thumbnail = videoMetadata.thumbnail; - } catch (e) { - console.warn('Failed to get video metadata:', e); + try { + const videoInfo = await this._getVideoMetadata(file); + thumbnail = videoInfo.thumbnail; + + if (options.includeMetadata) { + resolution = videoInfo.resolution; + duration = videoInfo.duration; } + } catch (e) { + console.warn('Failed to get video metadata:', e); } const result: MediaResult = { From 17525f6340c92fcc7e9d1450fa322e3957e8d267 Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Wed, 25 Mar 2026 10:15:04 -0400 Subject: [PATCH 22/26] fix: remove resolution from if condition --- src/web.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/web.ts b/src/web.ts index d571a7e..c7ac9e2 100644 --- a/src/web.ts +++ b/src/web.ts @@ -340,7 +340,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { saved: false, }; - if (options.includeMetadata && resolution) { + if (options.includeMetadata) { result.metadata = { format, resolution, From 02bd832dd3168cb55fe91e87c9ce86bfe22178ad Mon Sep 17 00:00:00 2001 From: Alexandre Jacinto Date: Wed, 25 Mar 2026 10:34:33 -0400 Subject: [PATCH 23/26] chore: run prettier to fix lint issues --- src/web.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/web.ts b/src/web.ts index c7ac9e2..62ca1ec 100644 --- a/src/web.ts +++ b/src/web.ts @@ -98,7 +98,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { (photo) => this._getCameraPhoto(photo, options), () => this.fileInputExperience(options, resolve, reject), resolve, - reject + reject, ); } @@ -257,7 +257,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { (photo) => this._buildPhotoMediaResult(photo, options.includeMetadata ?? false), () => this.takePhotoCameraInputExperience(options, resolve, reject), resolve, - reject + reject, ); } @@ -428,7 +428,7 @@ export class CameraWeb extends WebPlugin implements CameraPlugin { onPhotoCallback: (photo: Blob) => Promise, fallbackCallback: () => void, resolve: any, - reject: any + reject: any, ): Promise { if (customElements.get('pwa-camera-modal')) { const cameraModal: any = document.createElement('pwa-camera-modal'); From 87c4740119d40dbc566e6684fd2a4716cd1d5309 Mon Sep 17 00:00:00 2001 From: Rui Mendes Date: Wed, 25 Mar 2026 15:20:30 +0000 Subject: [PATCH 24/26] added new video parameters and remove videoSettings --- .../plugins/camera/IonCameraFlow.kt | 30 +++++++++---------- .../plugins/camera/IonVideoSettings.kt | 7 ----- 2 files changed, 14 insertions(+), 23 deletions(-) delete mode 100644 android/src/main/java/com/capacitorjs/plugins/camera/IonVideoSettings.kt diff --git a/android/src/main/java/com/capacitorjs/plugins/camera/IonCameraFlow.kt b/android/src/main/java/com/capacitorjs/plugins/camera/IonCameraFlow.kt index c46305f..fc926ef 100644 --- a/android/src/main/java/com/capacitorjs/plugins/camera/IonCameraFlow.kt +++ b/android/src/main/java/com/capacitorjs/plugins/camera/IonCameraFlow.kt @@ -33,6 +33,7 @@ import io.ionic.libs.ioncameralib.manager.IONCAMRGalleryManager import io.ionic.libs.ioncameralib.manager.IONCAMRVideoManager import io.ionic.libs.ioncameralib.model.IONCAMRCameraParameters import io.ionic.libs.ioncameralib.model.IONCAMREditParameters +import io.ionic.libs.ioncameralib.model.IONCAMRVideoParameters import io.ionic.libs.ioncameralib.model.IONCAMRError import io.ionic.libs.ioncameralib.model.IONCAMRMediaResult import io.ionic.libs.ioncameralib.model.IONCAMRMediaType @@ -62,11 +63,9 @@ class IonCameraFlow( private lateinit var editLauncher: ActivityResultLauncher private var currentCall: PluginCall? = null private var cameraSettings: IonCameraSettings? = null - private var videoSettings: IonVideoSettings? = null private var gallerySettings: IonGallerySettings? = null - private var editParameters = IONCAMREditParameters( - editURI = "", fromUri = false, saveToGallery = false, includeMetadata = false - ) + private var editParameters: IONCAMREditParameters? = null + private var videoParameters: IONCAMRVideoParameters? = null private var lastEditUri: String? = null companion object { @@ -118,7 +117,7 @@ class IonCameraFlow( } fun recordVideo(call: PluginCall) { - videoSettings = getVideoSettings(call) + videoParameters = getVideoSettings(call) currentCall = call openRecordVideo(call) } @@ -186,8 +185,8 @@ class IonCameraFlow( } - fun getVideoSettings(call: PluginCall): IonVideoSettings { - return IonVideoSettings( + fun getVideoSettings(call: PluginCall): IONCAMRVideoParameters { + return IONCAMRVideoParameters( saveToGallery = call.getBoolean("saveToGallery") ?: false, includeMetadata = call.getBoolean("includeMetadata") ?: false, isPersistent = call.getBoolean("isPersistent") ?: true @@ -273,7 +272,7 @@ class IonCameraFlow( } fun openRecordVideo(call: PluginCall) { - val settings = videoSettings ?: run { + val settings = videoParameters ?: run { sendError(IONCAMRError.INVALID_ARGUMENT_ERROR) return } @@ -344,14 +343,9 @@ class IonCameraFlow( return } - editParameters = IONCAMREditParameters( - "", - fromUri = false, - saveToGallery = false, - includeMetadata = false - ) val imageBase64 = call.getString("inputImage") if (imageBase64 == null) return + editParameters = null manager.editImage(activity, imageBase64, editLauncher) } @@ -857,7 +851,7 @@ class IonCameraFlow( sendError(IONCAMRError.CAPTURE_VIDEO_ERROR) return } - val settings = videoSettings ?: run { + val settings = videoParameters ?: run { sendError(IONCAMRError.INVALID_ARGUMENT_ERROR) return } @@ -908,10 +902,14 @@ class IonCameraFlow( return } + val params = editParameters ?: IONCAMREditParameters( + editURI = "", fromUri = false, saveToGallery = false, includeMetadata = false + ) + manager.processResultFromEdit( activity, result.data, - editParameters, + params, { image -> handleEditBase64Result(image) }, diff --git a/android/src/main/java/com/capacitorjs/plugins/camera/IonVideoSettings.kt b/android/src/main/java/com/capacitorjs/plugins/camera/IonVideoSettings.kt deleted file mode 100644 index 5da78d4..0000000 --- a/android/src/main/java/com/capacitorjs/plugins/camera/IonVideoSettings.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.capacitorjs.plugins.camera - -data class IonVideoSettings( - val saveToGallery: Boolean = false, - val includeMetadata: Boolean = false, - val isPersistent: Boolean = true -) \ No newline at end of file From c53bb1aa54406e2970ec9de951c7873b4491fbd6 Mon Sep 17 00:00:00 2001 From: Rui Mendes Date: Wed, 25 Mar 2026 19:01:11 +0000 Subject: [PATCH 25/26] revert editParameters changes --- .../plugins/camera/IonCameraFlow.kt | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/android/src/main/java/com/capacitorjs/plugins/camera/IonCameraFlow.kt b/android/src/main/java/com/capacitorjs/plugins/camera/IonCameraFlow.kt index fc926ef..15d91e5 100644 --- a/android/src/main/java/com/capacitorjs/plugins/camera/IonCameraFlow.kt +++ b/android/src/main/java/com/capacitorjs/plugins/camera/IonCameraFlow.kt @@ -64,7 +64,9 @@ class IonCameraFlow( private var currentCall: PluginCall? = null private var cameraSettings: IonCameraSettings? = null private var gallerySettings: IonGallerySettings? = null - private var editParameters: IONCAMREditParameters? = null + private var editParameters = IONCAMREditParameters( + editURI = "", fromUri = false, saveToGallery = false, includeMetadata = false + ) private var videoParameters: IONCAMRVideoParameters? = null private var lastEditUri: String? = null @@ -343,9 +345,15 @@ class IonCameraFlow( return } + editParameters = IONCAMREditParameters( + "", + fromUri = false, + saveToGallery = false, + includeMetadata = false + ) + val imageBase64 = call.getString("inputImage") if (imageBase64 == null) return - editParameters = null manager.editImage(activity, imageBase64, editLauncher) } @@ -902,14 +910,10 @@ class IonCameraFlow( return } - val params = editParameters ?: IONCAMREditParameters( - editURI = "", fromUri = false, saveToGallery = false, includeMetadata = false - ) - manager.processResultFromEdit( activity, result.data, - params, + editParameters, { image -> handleEditBase64Result(image) }, From 72efd811cc363d57e3ac47f3fffe619f93ded138 Mon Sep 17 00:00:00 2001 From: Rui Mendes Date: Thu, 26 Mar 2026 12:15:29 +0000 Subject: [PATCH 26/26] remove optinal on saved --- src/definitions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/definitions.ts b/src/definitions.ts index c3b189b..ea5e461 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -722,7 +722,7 @@ export interface Photo { * * @since 1.1.0 */ - saved?: boolean; + saved: boolean; } export interface GalleryPhotos {