[Video] Upload errors and UI improvements (#5092)
* surface errors in UI * style progress indicator * remove job status progress * rm log * fix webm ext
This commit is contained in:
parent
f9d736653c
commit
0e1de19903
11 changed files with 155 additions and 60 deletions
|
@ -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}`)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue