import { ApolloClient, useApolloClient } from "@apollo/client"; import { useIsFocused } from "@react-navigation/native"; import * as gql from "railbird-gql"; import React, { useCallback, useEffect, useRef, useState } from "react"; import { Button, StyleSheet, Text, View } from "react-native"; import * as RNFS from "react-native-fs"; import { Camera, CameraRuntimeError, Orientation, PhotoFile, VideoFile, useCameraDevice, useCameraFormat, useCameraPermission, } from "react-native-vision-camera"; import { RecordingButton } from "./capture-button"; import { useIsForeground } from "./is-foreground"; type Dictionary = { [key in KeyType]: ValueType; }; class StreamUploadManager { client: ApolloClient; videoId: number; nextUploadIdToRequest: number = 0; highestUploadLinkObtained: number = -1; prefetchedUploadLinks: Dictionary = {}; uploadQueue: Array<() => Promise> = []; isUploading: boolean = false; constructor(client: ApolloClient, videoId: number) { this.client = client; this.videoId = videoId; } enqueueUploadTask(task: () => Promise) { this.uploadQueue.push(task); this.processUploadQueue(); } async processUploadQueue() { if (this.isUploading || this.uploadQueue.length === 0) { return; } this.isUploading = true; const task = this.uploadQueue.shift(); try { if (task) { await task(); } } catch (error) { console.error("Error processing upload task", error); } finally { this.isUploading = false; this.processUploadQueue(); } } async uploadChunk({ filepath, index }: { filepath: string; index: number }) { this.enqueueUploadTask(async () => { const uploadUrl = await this.getUploadLink(index); const uploadRequest = RNFS.uploadFiles({ toUrl: uploadUrl, files: [{ filepath: filepath }], method: "PUT", binaryStreamOnly: true, headers: { "Content-Type": "application/octet-stream", }, begin: (res) => { console.log("Start upload", res); }, progress: (res) => { console.log("Uploading", res); }, }); console.log(JSON.stringify(uploadRequest)); const result = await uploadRequest.promise; if (result.statusCode === 200) { console.log(`${filepath} Uploaded`); } else { console.error("SERVER ERROR"); } }); } async getUploadLink(chunkId: number): Promise { if (this.prefetchedUploadLinks[chunkId]) { return this.prefetchedUploadLinks[chunkId]; } return this.requestUploadLink(chunkId); } async requestUploadLink(chunkId: number): Promise { console.log(`Requesting upload link for chunk ${chunkId}`); const result = await this.client.mutate({ mutation: gql.GetUploadLinkDocument, variables: { videoId: this.videoId, chunkIndex: chunkId }, }); this.prefetchedUploadLinks[chunkId] = result.data.getUploadLink.uploadUrl; return result.data.getUploadLink.uploadUrl; } } export default function CameraScreen({ route, navigation, }): React.ReactElement { const apolloClient = useApolloClient(); const [createUpload, { data, loading, error }] = gql.useCreateUploadStreamMutation(); const [uploadManager, setUploadManager] = useState(null); useEffect(() => { if ( data && data.createUploadStream && data.createUploadStream.videoId && !uploadManager ) { const newVideoId = data.createUploadStream.videoId; console.log(`VideoId: ${newVideoId}`); setUploadManager(new StreamUploadManager(apolloClient, newVideoId)); } }, [data, uploadManager]); const camera = useRef(null); const { hasPermission, requestPermission } = useCameraPermission(); const [isCameraInitialized, setIsCameraInitialized] = useState(false); const isForeground = useIsForeground(); const isFocused = useIsFocused(); const isActive = isForeground && isFocused; const onError = useCallback((error: CameraRuntimeError) => { console.error(error); }, []); const onInitialized = useCallback(() => { console.log("Camera initialized!"); setIsCameraInitialized(true); createUpload({ variables: { videoName: "Test" } }); }, []); const onMediaCaptured = useCallback((media: PhotoFile | VideoFile) => { console.log(`Media captured! ${JSON.stringify(media)}`); }, []); const onVideoChunkReady = useCallback( (event) => { console.log( `Chunk ready in react-native ${JSON.stringify(event.nativeEvent)}`, ); uploadManager.uploadChunk(event.nativeEvent); }, [uploadManager], ); if (!hasPermission) { requestPermission(); // Error handling in case they refuse to give permission } const device = useCameraDevice("back"); const format = useCameraFormat(device, [ { videoResolution: { width: 1920, height: 1080 } }, { fps: 30 }, ]); // TODO(#60): setOrientation should be called when changes are detected const [orientation, setOrientation] = useState("portrait"); const toggleOrientation = () => { setOrientation((currentOrientation) => currentOrientation === "landscape-left" ? "portrait" : "landscape-left", ); }; // Replace with error handling if (device === null) { console.log(device); return ( Camera not available. Does user have permissions: {hasPermission} ); } return ( hasPermission && (