Handle conversion of too-big assets on web

This commit is contained in:
Eric Bailey
2025-08-15 14:55:37 -05:00
parent b554a388b5
commit 28e5ff0e7a
+115 -32
View File
@@ -8,7 +8,7 @@ import {
import {msg, plural} from '@lingui/macro' import {msg, plural} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {VIDEO_MAX_DURATION_MS} from '#/lib/constants' import {VIDEO_MAX_DURATION_MS, VIDEO_MAX_SIZE} from '#/lib/constants'
import { import {
usePhotoLibraryPermission, usePhotoLibraryPermission,
useVideoLibraryPermission, useVideoLibraryPermission,
@@ -60,6 +60,7 @@ enum SelectedAssetError {
MaxImages = 'MaxImages', MaxImages = 'MaxImages',
MaxVideos = 'MaxVideos', MaxVideos = 'MaxVideos',
VideoTooLong = 'VideoTooLong', VideoTooLong = 'VideoTooLong',
FileTooBig = 'FileTooBig',
MaxGIFs = 'MaxGIFs', MaxGIFs = 'MaxGIFs',
} }
@@ -185,12 +186,6 @@ function classifyImagePickerAsset(asset: ImagePickerAsset):
type = 'image' type = 'image'
} }
console.log({
asset,
type,
mimeType,
})
/* /*
* If we weren't able to find a valid type, we don't support this asset. * If we weren't able to find a valid type, we don't support this asset.
*/ */
@@ -209,22 +204,82 @@ function classifyImagePickerAsset(asset: ImagePickerAsset):
} }
} }
/* function dataURItoBlob(uri: string, mimeType: string) {
* WEB ONLY. On web, certain file formats (like `.mov`) don't give us a const [, data] = uri.split(',')
* duration or dimensions, so we need to load the file manually to extract const binary = atob(data)
* this. // Convert to array of bytes
*/ const array = new Uint8Array(binary.length)
async function getAdditionalVideoMetadata(asset: ValidatedImagePickerAsset) { for (let i = 0; i < binary.length; i++) {
if (isNative) return asset array[i] = binary.charCodeAt(i)
const file = await fetch(asset.uri) }
.then(res => res.blob()) // Create and return the Blob
.then( return new Blob([array], {type: mimeType})
blob => }
new File([blob], `tmp.${mimeToExt(asset.mimeType)}`, {
type: asset.mimeType, export enum GetMetadataError {
}), FileTooLarge = 'FileTooLarge',
) UnknownExtension = 'UnknownExtension',
return await getVideoMetadata(file) FileCreationFailure = 'FileCreationFailure',
MetadataExtractionFailure = 'MetadataExtractionFailure',
}
async function getMetadata(
uri: string,
mimeType?: string,
): Promise<
| {
error: GetMetadataError
asset: undefined
}
| {
error: undefined
asset: ImagePickerAsset
}
> {
const mime = mimeType || extractDataUriMime(uri)
const blob = dataURItoBlob(uri, mime)
if (blob.size > VIDEO_MAX_SIZE) {
return {
error: GetMetadataError.FileTooLarge,
asset: undefined,
}
}
const ext = mimeToExt(mime)
if (!ext) {
return {
error: GetMetadataError.UnknownExtension,
asset: undefined,
}
}
const file = new File([blob], `tmp.${ext}`, {
type: mime,
})
if (!file)
return {
error: GetMetadataError.FileCreationFailure,
asset: undefined,
}
try {
const asset = await getVideoMetadata(file)
return {
error: undefined,
asset,
}
} catch (e) {
logger.error(`getMetadata: failed to get file metadata`, {
safeMessage: e instanceof Error ? e.message : String(e),
})
return {
error: GetMetadataError.MetadataExtractionFailure,
asset: undefined,
}
}
} }
/** /**
@@ -322,17 +377,30 @@ async function processImagePickerAssets(
const selectedVideo = supportedAssets[0] const selectedVideo = supportedAssets[0]
if (typeof selectedVideo.duration !== 'number') { if (typeof selectedVideo.duration !== 'number') {
try { /*
const metadata = await getAdditionalVideoMetadata(selectedVideo) * We can only do this on web
selectedVideo.duration = metadata.duration */
selectedVideo.width = metadata.width if (isWeb) {
selectedVideo.height = metadata.height const {error, asset} = await getMetadata(
} catch (e: any) { selectedVideo.uri,
logger.error(`processSelectedAssets: failed to get video metadata`, { selectedVideo.mimeType,
safeMessage: e.message, )
}) if (error) {
switch (error) {
case GetMetadataError.FileTooLarge:
errors.add(SelectedAssetError.FileTooBig)
supportedAssets = []
break
default:
errors.add(SelectedAssetError.Unsupported) errors.add(SelectedAssetError.Unsupported)
supportedAssets = [] supportedAssets = []
break
}
} else {
selectedVideo.duration = asset.duration
selectedVideo.width = asset.width
selectedVideo.height = asset.height
}
} }
} else { } else {
/* /*
@@ -382,6 +450,12 @@ export function SelectMediaButton({
const processSelectedAssets = useCallback( const processSelectedAssets = useCallback(
async (rawAssets: ImagePickerAsset[]) => { async (rawAssets: ImagePickerAsset[]) => {
// const {uri, ...rest} = rawAssets[0] || {}
// alert(JSON.stringify({
// uri: uri.slice(0, 40),
// ...rest
// }, null, 2))
// return
const { const {
type, type,
assets, assets,
@@ -419,6 +493,9 @@ export function SelectMediaButton({
[SelectedAssetError.MaxGIFs]: _( [SelectedAssetError.MaxGIFs]: _(
msg`You can only select one GIF at a time.`, msg`You can only select one GIF at a time.`,
), ),
[SelectedAssetError.FileTooBig]: _(
msg`This file is too large. Maximum size is 100mb.`,
),
}[error] }[error]
}) })
@@ -470,6 +547,12 @@ export function SelectMediaButton({
if (canceled) return if (canceled) return
await processSelectedAssets(assets) await processSelectedAssets(assets)
// if (isNative) {
// } else if (isWeb) {
// const {assets, canceled} = await pickVideo()
// await processSelectedAssets(assets)
// }
}, [ }, [
_, _,
requestPhotoAccessIfNeeded, requestPhotoAccessIfNeeded,