diff options
Diffstat (limited to 'src/state/queries')
-rw-r--r-- | src/state/queries/video/util.ts | 13 | ||||
-rw-r--r-- | src/state/queries/video/video-upload.ts | 17 | ||||
-rw-r--r-- | src/state/queries/video/video-upload.web.ts | 21 | ||||
-rw-r--r-- | src/state/queries/video/video.ts | 91 |
4 files changed, 97 insertions, 45 deletions
diff --git a/src/state/queries/video/util.ts b/src/state/queries/video/util.ts index db58b60c1..898f1736d 100644 --- a/src/state/queries/video/util.ts +++ b/src/state/queries/video/util.ts @@ -24,3 +24,16 @@ export function useVideoAgent() { }) }, []) } + +export function mimeToExt(mimeType: string) { + switch (mimeType) { + case 'video/mp4': + return 'mp4' + case 'video/webm': + return 'webm' + case 'video/mpeg': + return 'mpeg' + default: + throw new Error(`Unsupported mime type: ${mimeType}`) + } +} diff --git a/src/state/queries/video/video-upload.ts b/src/state/queries/video/video-upload.ts index 6fdd9d5bb..23e04316e 100644 --- a/src/state/queries/video/video-upload.ts +++ b/src/state/queries/video/video-upload.ts @@ -1,11 +1,14 @@ import {createUploadTask, FileSystemUploadType} from 'expo-file-system' import {AppBskyVideoDefs} from '@atproto/api' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' import {useMutation} from '@tanstack/react-query' import {nanoid} from 'nanoid/non-secure' import {cancelable} from '#/lib/async/cancelable' +import {ServerError} from '#/lib/media/video/errors' import {CompressedVideo} from '#/lib/media/video/types' -import {createVideoEndpointUrl} from '#/state/queries/video/util' +import {createVideoEndpointUrl, mimeToExt} from '#/state/queries/video/util' import {useAgent, useSession} from '#/state/session' import {getServiceAuthAudFromUrl} from 'lib/strings/url-helpers' @@ -22,13 +25,14 @@ export const useUploadVideoMutation = ({ }) => { const {currentAccount} = useSession() const agent = useAgent() + const {_} = useLingui() return useMutation({ mutationKey: ['video', 'upload'], mutationFn: cancelable(async (video: CompressedVideo) => { const uri = createVideoEndpointUrl('/xrpc/app.bsky.video.uploadVideo', { did: currentAccount!.did, - name: `${nanoid(12)}.mp4`, + name: `${nanoid(12)}.${mimeToExt(video.mimeType)}`, }) const serviceAuthAud = getServiceAuthAudFromUrl(agent.dispatchUrl) @@ -50,7 +54,7 @@ export const useUploadVideoMutation = ({ video.uri, { headers: { - 'content-type': 'video/mp4', + 'content-type': video.mimeType, Authorization: `Bearer ${serviceAuth.token}`, }, httpMethod: 'POST', @@ -65,6 +69,13 @@ export const useUploadVideoMutation = ({ } const responseBody = JSON.parse(res.body) as AppBskyVideoDefs.JobStatus + + if (!responseBody.jobId) { + throw new ServerError( + responseBody.error || _(msg`Failed to upload video`), + ) + } + return responseBody }, signal), onError, diff --git a/src/state/queries/video/video-upload.web.ts b/src/state/queries/video/video-upload.web.ts index c3ad39268..40f586450 100644 --- a/src/state/queries/video/video-upload.web.ts +++ b/src/state/queries/video/video-upload.web.ts @@ -1,10 +1,13 @@ import {AppBskyVideoDefs} from '@atproto/api' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' import {useMutation} from '@tanstack/react-query' import {nanoid} from 'nanoid/non-secure' import {cancelable} from '#/lib/async/cancelable' +import {ServerError} from '#/lib/media/video/errors' import {CompressedVideo} from '#/lib/media/video/types' -import {createVideoEndpointUrl} from '#/state/queries/video/util' +import {createVideoEndpointUrl, mimeToExt} from '#/state/queries/video/util' import {useAgent, useSession} from '#/state/session' import {getServiceAuthAudFromUrl} from 'lib/strings/url-helpers' @@ -21,13 +24,14 @@ export const useUploadVideoMutation = ({ }) => { const {currentAccount} = useSession() const agent = useAgent() + const {_} = useLingui() return useMutation({ mutationKey: ['video', 'upload'], mutationFn: cancelable(async (video: CompressedVideo) => { const uri = createVideoEndpointUrl('/xrpc/app.bsky.video.uploadVideo', { did: currentAccount!.did, - name: `${nanoid(12)}.mp4`, // @TODO: make sure it's always mp4' + name: `${nanoid(12)}.${mimeToExt(video.mimeType)}`, }) const serviceAuthAud = getServiceAuthAudFromUrl(agent.dispatchUrl) @@ -63,23 +67,24 @@ export const useUploadVideoMutation = ({ xhr.responseText, ) as AppBskyVideoDefs.JobStatus resolve(uploadRes) - onSuccess(uploadRes) } else { - reject() - onError(new Error('Failed to upload video')) + reject(new ServerError(_(msg`Failed to upload video`))) } } xhr.onerror = () => { - reject() - onError(new Error('Failed to upload video')) + reject(new ServerError(_(msg`Failed to upload video`))) } xhr.open('POST', uri) - xhr.setRequestHeader('Content-Type', 'video/mp4') + xhr.setRequestHeader('Content-Type', video.mimeType) xhr.setRequestHeader('Authorization', `Bearer ${serviceAuth.token}`) xhr.send(bytes) }, ) + if (!res.jobId) { + throw new ServerError(res.error || _(msg`Failed to upload video`)) + } + return res }, signal), onError, diff --git a/src/state/queries/video/video.ts b/src/state/queries/video/video.ts index 3c5094c71..87f315640 100644 --- a/src/state/queries/video/video.ts +++ b/src/state/queries/video/video.ts @@ -6,7 +6,8 @@ import {useLingui} from '@lingui/react' import {QueryClient, useQuery, useQueryClient} from '@tanstack/react-query' import {logger} from '#/logger' -import {VideoTooLargeError} from 'lib/media/video/errors' +import {isWeb} from '#/platform/detection' +import {ServerError, VideoTooLargeError} from 'lib/media/video/errors' import {CompressedVideo} from 'lib/media/video/types' import {useCompressVideoMutation} from 'state/queries/video/compress-video' import {useVideoAgent} from 'state/queries/video/util' @@ -58,7 +59,12 @@ function reducer(queryClient: QueryClient) { abortController: new AbortController(), } } else if (action.type === 'SetAsset') { - updatedState = {...state, asset: action.asset} + updatedState = { + ...state, + asset: action.asset, + status: 'compressing', + error: undefined, + } } else if (action.type === 'SetDimensions') { updatedState = { ...state, @@ -67,11 +73,11 @@ function reducer(queryClient: QueryClient) { : undefined, } } else if (action.type === 'SetVideo') { - updatedState = {...state, video: action.video} + updatedState = {...state, video: action.video, status: 'uploading'} } else if (action.type === 'SetJobStatus') { updatedState = {...state, jobStatus: action.jobStatus} } else if (action.type === 'SetBlobRef') { - updatedState = {...state, blobRef: action.blobRef} + updatedState = {...state, blobRef: action.blobRef, status: 'done'} } return updatedState } @@ -108,10 +114,6 @@ export function useUploadVideo({ type: 'SetBlobRef', blobRef, }) - dispatch({ - type: 'SetStatus', - status: 'idle', - }) onSuccess() }, }) @@ -125,10 +127,17 @@ export function useUploadVideo({ setJobId(response.jobId) }, onError: e => { - dispatch({ - type: 'SetError', - error: _(msg`An error occurred while uploading the video.`), - }) + if (e instanceof ServerError) { + dispatch({ + type: 'SetError', + error: e.message, + }) + } else { + dispatch({ + type: 'SetError', + error: _(msg`An error occurred while uploading the video.`), + }) + } logger.error('Error uploading video', {safeMessage: e}) }, setProgress: p => { @@ -141,6 +150,13 @@ export function useUploadVideo({ onProgress: p => { dispatch({type: 'SetProgress', progress: p}) }, + onSuccess: (video: CompressedVideo) => { + dispatch({ + type: 'SetVideo', + video, + }) + onVideoCompressed(video) + }, onError: e => { if (e instanceof VideoTooLargeError) { dispatch({ @@ -150,36 +166,28 @@ export function useUploadVideo({ } else { dispatch({ type: 'SetError', - // @TODO better error message from server, left untranslated on purpose - error: 'An error occurred while compressing the video.', + error: _(msg`An error occurred while compressing the video.`), }) logger.error('Error compressing video', {safeMessage: e}) } }, - onSuccess: (video: CompressedVideo) => { - dispatch({ - type: 'SetVideo', - video, - }) - dispatch({ - type: 'SetStatus', - status: 'uploading', - }) - onVideoCompressed(video) - }, signal: state.abortController.signal, }) const selectVideo = (asset: ImagePickerAsset) => { - dispatch({ - type: 'SetAsset', - asset, - }) - dispatch({ - type: 'SetStatus', - status: 'compressing', - }) - onSelectVideo(asset) + switch (getMimeType(asset)) { + case 'video/mp4': + case 'video/mpeg': + case 'video/webm': + dispatch({ + type: 'SetAsset', + asset, + }) + onSelectVideo(asset) + break + default: + throw new Error(_(msg`Unsupported video type: ${getMimeType(asset)}`)) + } } const clearVideo = () => { @@ -241,6 +249,21 @@ const useUploadStatusQuery = ({ isError, setJobId: (_jobId: string) => { setJobId(_jobId) + setEnabled(true) }, } } + +function getMimeType(asset: ImagePickerAsset) { + if (isWeb) { + const [mimeType] = asset.uri.slice('data:'.length).split(';base64,') + if (!mimeType) { + throw new Error('Could not determine mime type') + } + return mimeType + } + if (!asset.mimeType) { + throw new Error('Could not determine mime type') + } + return asset.mimeType +} |