ryannnnnn
ryannnnnn

Reputation: 49

On iOS my images load way too slow. I am using expo image picker with Firebase cloud storage with xhr blob conversion

On Android it hasn't shown to be an issue but iOS takes a full minute to display images when taken with camera/gallery to create a new post AND also in a FlatList render.

Has anyone else had this issue or know how to optimize it?

I am using xhr to convert to a blob which I am wondering if that might be the issue because I do set a loading spinner which doesn't show for a minute then it does and it loads fast.

This is a react native app built with expo using Firebase storage I'm converting the image to a download URL and saving to a Firestore document.

Here is my image handler:

function ImageSelector({
onTakeImage,
editImage,
basePath,
clearImage,
}: {
    onTakeImage: (image: ImageType) => void
    clearImage: boolean
    editImage?: {url: string, type: string} | null
    basePath: string
}) {
    const [image, setImage] = useState<{
        url: string
        type: string
    } | null>(editImage ? editImage : null)
    const [showDurationErr, setShowDurationErr] = useState<boolean>(false)
    const [galleryImage, setGalleryImage] = useState<{
        url: string
        type: string
    } | null>(null)
    const [cameraPermissionInfo, requestPermission] = useCameraPermissions()
    const [mediaStatus, requestPermissionMedia] = useMediaLibraryPermissions()
    const [uploadStatus, setUploadStatus] = useState<number>(-1)
    const video = useRef(null)
    const [status, setStatus] = useState<AVPlaybackStatusSuccess | {}>({})

async function verifyPermissions() {
    if (cameraPermissionInfo?.status === PermissionStatus.UNDETERMINED) {
        const permission = await requestPermission()
        return permission.granted
    }
    if (cameraPermissionInfo?.status === PermissionStatus.DENIED) {
        Alert.alert(
            "Permission was denied",
            "Camera permissions are required to use this app."
        )
        return false
    }

    return true
}

useEffect(() => {
    if (clearImage) {
        setImage(null)
    }
}, [clearImage])

useEffect(() => {
    if (!cameraPermissionInfo?.granted) verifyPermissions()
    if(!mediaStatus?.granted) verifyGalleryPermissions()
}, [])

async function verifyGalleryPermissions() {
    if (mediaStatus?.status === PermissionStatus.UNDETERMINED) {
        const permission = await requestPermissionMedia()
        return permission.granted
    }
    if (mediaStatus?.status === PermissionStatus.DENIED) {
        Alert.alert(
            "Permission was denied",
            "Gallery permissions will be needed to use this app."
        )
        return false
    }

    return true
}

async function imageTakerHandler() {
    const hasPermission = await verifyPermissions()

    if (!hasPermission) {
        return
    }
    const generatedFileId = uuid.v4()
    const image = await launchCameraAsync({
        mediaTypes: MediaTypeOptions.All,
        allowsEditing: true,
        // aspect: [16, 9],
        quality: 0.6,
    })
    console.log(image)
    async function getDownloadURL(){
        console.log(image)
        if(image.canceled){
            setShowDurationErr(true)
            return
        }
        if (image.assets) {
            //fetch and react native - in RN .57+ swtiched the default xhr.responseType from blob to text thus blobModules uriandler doesnt kick in and since
            //okhttp doesnt recognize url of file:// scheme it throws and exception.  Something about a blob leak so use blob.close()
            try {
                const uri = image.assets[0].uri
                
                const blob = await new Promise((resolve, reject) => {
                    Toast.show({
                        type: "success",
                        text1: "your blob",
                        text2: 'has started',
                    })
                    const xhr = new XMLHttpRequest()
                    xhr.onload = function () {
                        resolve(xhr.response)
                    }
                    xhr.onerror = function () {
                        reject(new TypeError("Network request failed"))
                    }
                    xhr.responseType = "blob"
                    xhr.open("get", uri, true)
                    xhr.send(null)
                })

                const downloadUrl = await FirebaseStorageService.uploadFile(
                    blob as Blob,
                    `${basePath}/${generatedFileId}`,
                    setUploadStatus
                )
                return downloadUrl
            } catch (error: any) {
                console.log(error?.message, "downloadImageurl error")
            }finally{
                Toast.show({
                    type: "congrats",
                    text1: "your blob",
                    text2: 'has completed',
                })
            }
        }
    }
    getDownloadURL().then((response) => {
        if (response && image.assets) {
            setImage({
                url: response as string,
                type: image.assets[0].type!,
            })
            onTakeImage({
                url: response as string,
                type: image.assets[0].type!,
            })
        }
    }).then(() => setUploadStatus(-1))
    }



async function imageSelectorHandler() {
    setShowDurationErr(false)
    setGalleryImage(null)
    const hasPermissionforGallery = await verifyGalleryPermissions()
    if (!hasPermissionforGallery) {
        return
    }
    const generatedFileId = uuid.v4()
    const galleryImage = await launchImageLibraryAsync({
        mediaTypes: MediaTypeOptions.All,
        allowsEditing: true,
        // aspect: [16, 9],
        quality: 0.6,
        videoMaxDuration: 120,
    })

    async function getDownloadURL() {
        if (
            galleryImage.canceled ||
            !galleryImage.assets ||
            (galleryImage.assets[0].duration &&
                galleryImage.assets[0]?.duration > 150000)
        ) {
            // User cancelled the image selection or no assets were returned

            setShowDurationErr(true)
            return
        }

        if (galleryImage.assets) {
            //fetch and react native - in RN .57+ swtiched the default xhr.responseType from blob to text thus blobModules uriandler doesnt kick in and since
            //okhttp doesnt recognize url of file:// scheme it throws and exception.  Something about a blob leak so use blob.close()
            try {
                const uri = galleryImage.assets[0].uri

                const blob = await new Promise((resolve, reject) => {
                    const xhr = new XMLHttpRequest()
                    xhr.onload = function () {
                        resolve(xhr.response)
                    }
                    xhr.onerror = function () {
                        reject(new TypeError("Network request failed"))
                    }
                    xhr.responseType = "blob"
                    xhr.open("get", uri, true)
                    xhr.send(null)
                })

                const downloadUrl = await FirebaseStorageService.uploadFile(
                    blob as Blob,
                    `${basePath}/${generatedFileId}`,
                    setUploadStatus
                )
                return downloadUrl
            } catch (error: any) {
                console.log(error?.message, "downloadImageurl error")
            }
        }
    }
    getDownloadURL().then((response) => {
        if (response && galleryImage.assets) {
            setGalleryImage({
                url: response as string,
                type: galleryImage.assets[0].type!,
            })
            onTakeImage({
                url: response as string,
                type: galleryImage.assets[0].type!,
            })
        }
    }).then(() => setUploadStatus(-1))
}

let imagePreview = (
    <Text
        style={{
            color: Colors.bone,
            marginVertical: 10,
            fontFamily: "Prata",
            fontSize: 17,
        }}
    >
        Image/Video
    </Text>
)

if (image) {
    imagePreview = (
        <View style={styles.imagePrev}>
            <Image style={styles.image} source={{ uri: image.url }} />
        </View>
    )
}

if (galleryImage) {
    if (galleryImage.type === "image") {
        imagePreview = (
            <View style={styles.imagePrev}>
                <Image
                    resizeMode="contain"
                    style={styles.image}
                    source={{ uri: galleryImage.url }}
                />
            </View>
        )
    } else if (galleryImage.type === "video") {
        imagePreview = (
            <View className="h-80 w-full mb-2 ">
                <Video
                    ref={video}
                    style={{ width: "100%", height: "100%" }}
                    source={{
                        uri: galleryImage.url,
                    }}
                    useNativeControls
                    resizeMode={ResizeMode.COVER}
                    isLooping
                    onPlaybackStatusUpdate={(status) =>
                        setStatus(() => status)
                    }
                />
            </View>
        )
    }
}

return (
    <ScrollView className="w-full">
        <View className="w-full">{imagePreview}</View>
        {uploadStatus > 0 &&
         <Progress.Circle
         className="self-center"
         progress={uploadStatus}
         animated={true}
         showsText={true}
         
         unfilledColor={Colors.primaryGreen}
         color={Colors.accentDark}
         />}
        <View style={styles.buttonContainer}>
            <Icon
                color={Colors.bone}
                name={"camera-enhance-outline"}
                onPress={imageTakerHandler}
                size={30}
                text="Camera"
            />
            {/* <PrimaryBtn text="Camera" onPress={imageTakerHandler} /> */}
            <Icon
                color={Colors.primaryLight}
                name={"view-gallery-outline"}
                onPress={imageSelectorHandler}
                size={30}
                text="Gallery"
            />
            {/* <AccentBtn text="Gallery" onPress={imageSelectorHandler} /> */}
        </View>
        {showDurationErr && (
            <Text className="text-center text-bone">
               Something went wrong, if video is longer than allotment of 2.5 minutes, please trim
                first.
            </Text>
        )}
    </ScrollView>
)

}

Upvotes: 2

Views: 257

Answers (0)

Related Questions