diff --git a/docs/docs/guides/TAKING_PHOTOS.mdx b/docs/docs/guides/TAKING_PHOTOS.mdx index a800503cd8..4d4a9dde9a 100644 --- a/docs/docs/guides/TAKING_PHOTOS.mdx +++ b/docs/docs/guides/TAKING_PHOTOS.mdx @@ -81,6 +81,40 @@ const photo = await camera.current.takePhoto({ Note that flash is only available on camera devices where [`hasFlash`](/docs/api/interfaces/CameraDevice#hasflash) is `true`; for example most front cameras don't have a flash. +### Thumbnail Generation + +For a better user experience, you can generate a low-resolution thumbnail that loads while the full photo is being processed and saved. This is especially useful for displaying a preview in your UI without waiting for the full-resolution image. + +To generate a thumbnail, provide the [`thumbnailSize`](/docs/api/interfaces/TakePhotoOptions#thumbnailsize) and [`onThumbnailReady`](/docs/api/interfaces/TakePhotoOptions#onthumbnailready) options: + +```tsx +const photo = await camera.current.takePhoto({ + thumbnailSize: { width: 200, height: 200 }, + onThumbnailReady: (thumbnail) => { + // Thumbnail is ready! Display it immediately + setThumbnailUri(`file://${thumbnail.path}`) + } +}) + +// Full photo is now ready +setPhotoUri(`file://${photo.path}`) +``` + +The `onThumbnailReady` callback is invoked as soon as the thumbnail is generated, which typically happens before the full photo is saved. This allows you to: +- Display a preview to the user immediately +- Show a loading state with the thumbnail while uploading the full image +- Reduce memory usage by rendering thumbnails in lists instead of full photos + +**Platform implementations:** +- **iOS**: Uses the embedded thumbnail from the camera capture if available for maximum performance +- **Android**: Uses memory-efficient downsampling with hardware-accelerated decoding (never loads the full image into memory) + +Both implementations are optimized for their respective platforms and generate thumbnails asynchronously without blocking photo capture. + +:::tip +The thumbnail is stored in a temporary directory just like the main photo. Remember to clean up temporary files when you're done with them. +::: + ### Photo Quality Balance The photo capture pipeline can be configured to prioritize speed over quality, quality over speed or balance both quality and speed using the [`photoQualityBalance`](/docs/api/interfaces/CameraProps#photoQualityBalance) prop. diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 863dcfc1a7..abe182bbd2 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -2098,8 +2098,8 @@ SPEC CHECKSUMS: RNVectorIcons: 182892e7d1a2f27b52d3c627eca5d2665a22ee28 SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d VisionCamera: 4146fa2612c154f893a42a9b1feedf868faa6b23 - Yoga: aa3df615739504eebb91925fc9c58b4922ea9a08 + Yoga: 055f92ad73f8c8600a93f0e25ac0b2344c3b07e6 PODFILE CHECKSUM: 2ad84241179871ca890f7c65c855d117862f1a68 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/example/src/CameraPage.tsx b/example/src/CameraPage.tsx index af65e71462..49ad15c5b9 100644 --- a/example/src/CameraPage.tsx +++ b/example/src/CameraPage.tsx @@ -4,7 +4,7 @@ import type { GestureResponderEvent } from 'react-native' import { StyleSheet, Text, View } from 'react-native' import type { PinchGestureHandlerGestureEvent } from 'react-native-gesture-handler' import { PinchGestureHandler, TapGestureHandler } from 'react-native-gesture-handler' -import type { CameraProps, CameraRuntimeError, PhotoFile, VideoFile } from 'react-native-vision-camera' +import type { CameraProps, CameraRuntimeError, PhotoFile, ThumbnailFile, VideoFile } from 'react-native-vision-camera' import { runAtTargetFps, useCameraDevice, @@ -55,6 +55,7 @@ export function CameraPage({ navigation }: Props): React.ReactElement { const [enableHdr, setEnableHdr] = useState(false) const [flash, setFlash] = useState<'off' | 'on'>('off') const [enableNightMode, setEnableNightMode] = useState(false) + const [thumbnail, setThumbnail] = useState(null) // camera device settings const [preferredDevice] = usePreferredCameraDevice() @@ -112,12 +113,15 @@ export function CameraPage({ navigation }: Props): React.ReactElement { const onMediaCaptured = useCallback( (media: PhotoFile | VideoFile, type: 'photo' | 'video') => { console.log(`Media captured! ${JSON.stringify(media)}`) + console.log(`Thumbnail: ${JSON.stringify(thumbnail)}`) + console.log(new Date()) navigation.navigate('MediaPage', { path: media.path, type: type, + thumbnail: thumbnail, }) }, - [navigation], + [navigation, thumbnail], ) const onFlipCameraPressed = useCallback(() => { setCameraPosition((p) => (p === 'back' ? 'front' : 'back')) @@ -178,6 +182,15 @@ export function CameraPage({ navigation }: Props): React.ReactElement { location.requestPermission() }, [location]) + const onThumbnailReady = useCallback( + (t: ThumbnailFile) => { + console.log(`=============thumbnail Ready=============\n${t.width}x${t.height}`) + console.log(new Date()) + setThumbnail(t) + }, + [setThumbnail], + ) + const frameProcessor = useFrameProcessor((frame) => { 'worklet' @@ -248,6 +261,7 @@ export function CameraPage({ navigation }: Props): React.ReactElement { flash={supportsFlash ? flash : 'off'} enabled={isCameraInitialized && isActive} setIsPressingButton={setIsPressingButton} + onThumbnailReady={onThumbnailReady} /> diff --git a/example/src/MediaPage.tsx b/example/src/MediaPage.tsx index 8eff60dd05..281a350f15 100644 --- a/example/src/MediaPage.tsx +++ b/example/src/MediaPage.tsx @@ -33,7 +33,7 @@ const isVideoOnLoadEvent = (event: OnLoadData | OnLoadImage): event is OnLoadDat type Props = NativeStackScreenProps export function MediaPage({ navigation, route }: Props): React.ReactElement { - const { path, type } = route.params + const { path, type, thumbnail } = route.params const [hasMediaLoaded, setHasMediaLoaded] = useState(false) const isForeground = useIsForeground() const isScreenFocused = useIsFocused() @@ -85,7 +85,10 @@ export function MediaPage({ navigation, route }: Props): React.ReactElement { return ( {type === 'photo' && ( - + <> + + {thumbnail !== null && } + )} {type === 'video' && (