diff options
Diffstat (limited to 'src/state/queries/video')
-rw-r--r-- | src/state/queries/video/util.ts | 11 | ||||
-rw-r--r-- | src/state/queries/video/video-upload.ts | 23 | ||||
-rw-r--r-- | src/state/queries/video/video-upload.web.ts | 69 | ||||
-rw-r--r-- | src/state/queries/video/video.ts | 60 |
4 files changed, 86 insertions, 77 deletions
diff --git a/src/state/queries/video/util.ts b/src/state/queries/video/util.ts index 266d8aee3..9224c776d 100644 --- a/src/state/queries/video/util.ts +++ b/src/state/queries/video/util.ts @@ -1,3 +1,6 @@ +import {useMemo} from 'react' +import {AtpAgent} from '@atproto/api' + const UPLOAD_ENDPOINT = process.env.EXPO_PUBLIC_VIDEO_ROOT_ENDPOINT ?? '' export const createVideoEndpointUrl = ( @@ -13,3 +16,11 @@ export const createVideoEndpointUrl = ( } return url.href } + +export function useVideoAgent() { + return useMemo(() => { + return new AtpAgent({ + service: UPLOAD_ENDPOINT, + }) + }, []) +} diff --git a/src/state/queries/video/video-upload.ts b/src/state/queries/video/video-upload.ts index cf741b251..d806249c9 100644 --- a/src/state/queries/video/video-upload.ts +++ b/src/state/queries/video/video-upload.ts @@ -1,20 +1,18 @@ import {createUploadTask, FileSystemUploadType} from 'expo-file-system' +import {AppBskyVideoDefs} from '@atproto/api' import {useMutation} from '@tanstack/react-query' import {nanoid} from 'nanoid/non-secure' import {CompressedVideo} from '#/lib/media/video/compress' -import {UploadVideoResponse} from '#/lib/media/video/types' import {createVideoEndpointUrl} from '#/state/queries/video/util' import {useAgent, useSession} from '#/state/session' -const UPLOAD_HEADER = process.env.EXPO_PUBLIC_VIDEO_HEADER ?? '' - export const useUploadVideoMutation = ({ onSuccess, onError, setProgress, }: { - onSuccess: (response: UploadVideoResponse) => void + onSuccess: (response: AppBskyVideoDefs.JobStatus) => void onError: (e: any) => void setProgress: (progress: number) => void }) => { @@ -23,7 +21,7 @@ export const useUploadVideoMutation = ({ return useMutation({ mutationFn: async (video: CompressedVideo) => { - const uri = createVideoEndpointUrl('/upload', { + const uri = createVideoEndpointUrl('/xrpc/app.bsky.video.uploadVideo', { did: currentAccount!.did, name: `${nanoid(12)}.mp4`, // @TODO what are we limiting this to? }) @@ -33,19 +31,19 @@ export const useUploadVideoMutation = ({ throw new Error('Agent does not have a PDS URL') } - const {data: serviceAuth} = - await agent.api.com.atproto.server.getServiceAuth({ + const {data: serviceAuth} = await agent.com.atproto.server.getServiceAuth( + { aud: `did:web:${agent.pdsUrl.hostname}`, lxm: 'com.atproto.repo.uploadBlob', - }) + }, + ) const uploadTask = createUploadTask( uri, video.uri, { headers: { - 'dev-key': UPLOAD_HEADER, - 'content-type': 'video/mp4', // @TODO same question here. does the compression step always output mp4? + 'content-type': 'video/mp4', Authorization: `Bearer ${serviceAuth.token}`, }, httpMethod: 'POST', @@ -59,10 +57,7 @@ export const useUploadVideoMutation = ({ throw new Error('No response') } - // @TODO rm, useful for debugging/getting video cid - console.log('[VIDEO]', res.body) - const responseBody = JSON.parse(res.body) as UploadVideoResponse - onSuccess(responseBody) + const responseBody = JSON.parse(res.body) as AppBskyVideoDefs.JobStatus return responseBody }, onError, diff --git a/src/state/queries/video/video-upload.web.ts b/src/state/queries/video/video-upload.web.ts index b9b0bacfa..09d107423 100644 --- a/src/state/queries/video/video-upload.web.ts +++ b/src/state/queries/video/video-upload.web.ts @@ -1,19 +1,17 @@ +import {AppBskyVideoDefs} from '@atproto/api' import {useMutation} from '@tanstack/react-query' import {nanoid} from 'nanoid/non-secure' import {CompressedVideo} from '#/lib/media/video/compress' -import {UploadVideoResponse} from '#/lib/media/video/types' import {createVideoEndpointUrl} from '#/state/queries/video/util' import {useAgent, useSession} from '#/state/session' -const UPLOAD_HEADER = process.env.EXPO_PUBLIC_VIDEO_HEADER ?? '' - export const useUploadVideoMutation = ({ onSuccess, onError, setProgress, }: { - onSuccess: (response: UploadVideoResponse) => void + onSuccess: (response: AppBskyVideoDefs.JobStatus) => void onError: (e: any) => void setProgress: (progress: number) => void }) => { @@ -22,9 +20,9 @@ export const useUploadVideoMutation = ({ return useMutation({ mutationFn: async (video: CompressedVideo) => { - const uri = createVideoEndpointUrl('/upload', { + const uri = createVideoEndpointUrl('/xrpc/app.bsky.video.uploadVideo', { did: currentAccount!.did, - name: `${nanoid(12)}.mp4`, // @TODO what are we limiting this to? + name: `${nanoid(12)}.mp4`, // @TODO: make sure it's always mp4' }) // a logged-in agent should have this set, but we'll check just in case @@ -32,46 +30,45 @@ export const useUploadVideoMutation = ({ throw new Error('Agent does not have a PDS URL') } - const {data: serviceAuth} = - await agent.api.com.atproto.server.getServiceAuth({ + const {data: serviceAuth} = await agent.com.atproto.server.getServiceAuth( + { aud: `did:web:${agent.pdsUrl.hostname}`, lxm: 'com.atproto.repo.uploadBlob', - }) + }, + ) const bytes = await fetch(video.uri).then(res => res.arrayBuffer()) const xhr = new XMLHttpRequest() - const res = (await new Promise((resolve, reject) => { - xhr.upload.addEventListener('progress', e => { - const progress = e.loaded / e.total - setProgress(progress) - }) - xhr.onloadend = () => { - if (xhr.readyState === 4) { - const uploadRes = JSON.parse( - xhr.responseText, - ) as UploadVideoResponse - resolve(uploadRes) - onSuccess(uploadRes) - } else { + const res = await new Promise<AppBskyVideoDefs.JobStatus>( + (resolve, reject) => { + xhr.upload.addEventListener('progress', e => { + const progress = e.loaded / e.total + setProgress(progress) + }) + xhr.onloadend = () => { + if (xhr.readyState === 4) { + const uploadRes = JSON.parse( + xhr.responseText, + ) as AppBskyVideoDefs.JobStatus + resolve(uploadRes) + onSuccess(uploadRes) + } else { + reject() + onError(new Error('Failed to upload video')) + } + } + xhr.onerror = () => { reject() onError(new Error('Failed to upload video')) } - } - xhr.onerror = () => { - reject() - onError(new Error('Failed to upload video')) - } - xhr.open('POST', uri) - xhr.setRequestHeader('Content-Type', 'video/mp4') // @TODO how we we set the proper content type? - // @TODO remove this header for prod - xhr.setRequestHeader('dev-key', UPLOAD_HEADER) - xhr.setRequestHeader('Authorization', `Bearer ${serviceAuth.token}`) - xhr.send(bytes) - })) as UploadVideoResponse + xhr.open('POST', uri) + xhr.setRequestHeader('Content-Type', 'video/mp4') + xhr.setRequestHeader('Authorization', `Bearer ${serviceAuth.token}`) + xhr.send(bytes) + }, + ) - // @TODO rm for prod - console.log('[VIDEO]', res) return res }, onError, diff --git a/src/state/queries/video/video.ts b/src/state/queries/video/video.ts index 295db38b4..64390801e 100644 --- a/src/state/queries/video/video.ts +++ b/src/state/queries/video/video.ts @@ -1,5 +1,6 @@ import React from 'react' import {ImagePickerAsset} from 'expo-image-picker' +import {AppBskyVideoDefs, BlobRef} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useQuery} from '@tanstack/react-query' @@ -7,37 +8,29 @@ import {useQuery} from '@tanstack/react-query' import {logger} from '#/logger' import {CompressedVideo} from 'lib/media/video/compress' import {VideoTooLargeError} from 'lib/media/video/errors' -import {JobState, JobStatus} from 'lib/media/video/types' import {useCompressVideoMutation} from 'state/queries/video/compress-video' -import {createVideoEndpointUrl} from 'state/queries/video/util' +import {useVideoAgent} from 'state/queries/video/util' import {useUploadVideoMutation} from 'state/queries/video/video-upload' type Status = 'idle' | 'compressing' | 'processing' | 'uploading' | 'done' type Action = - | { - type: 'SetStatus' - status: Status - } - | { - type: 'SetProgress' - progress: number - } - | { - type: 'SetError' - error: string | undefined - } + | {type: 'SetStatus'; status: Status} + | {type: 'SetProgress'; progress: number} + | {type: 'SetError'; error: string | undefined} | {type: 'Reset'} | {type: 'SetAsset'; asset: ImagePickerAsset} | {type: 'SetVideo'; video: CompressedVideo} - | {type: 'SetJobStatus'; jobStatus: JobStatus} + | {type: 'SetJobStatus'; jobStatus: AppBskyVideoDefs.JobStatus} + | {type: 'SetBlobRef'; blobRef: BlobRef} export interface State { status: Status progress: number asset?: ImagePickerAsset video: CompressedVideo | null - jobStatus?: JobStatus + jobStatus?: AppBskyVideoDefs.JobStatus + blobRef?: BlobRef error?: string } @@ -54,6 +47,7 @@ function reducer(state: State, action: Action): State { status: 'idle', progress: 0, video: null, + blobRef: undefined, } } else if (action.type === 'SetAsset') { updatedState = {...state, asset: action.asset} @@ -61,6 +55,8 @@ function reducer(state: State, action: Action): State { updatedState = {...state, video: action.video} } else if (action.type === 'SetJobStatus') { updatedState = {...state, jobStatus: action.jobStatus} + } else if (action.type === 'SetBlobRef') { + updatedState = {...state, blobRef: action.blobRef} } return updatedState } @@ -80,7 +76,7 @@ export function useUploadVideo({ }) const {setJobId} = useUploadStatusQuery({ - onStatusChange: (status: JobStatus) => { + onStatusChange: (status: AppBskyVideoDefs.JobStatus) => { // This might prove unuseful, most of the job status steps happen too quickly to even be displayed to the user // Leaving it for now though dispatch({ @@ -89,7 +85,11 @@ export function useUploadVideo({ }) setStatus(status.state.toString()) }, - onSuccess: () => { + onSuccess: blobRef => { + dispatch({ + type: 'SetBlobRef', + blobRef, + }) dispatch({ type: 'SetStatus', status: 'idle', @@ -104,7 +104,7 @@ export function useUploadVideo({ type: 'SetStatus', status: 'processing', }) - setJobId(response.job_id) + setJobId(response.jobId) }, onError: e => { dispatch({ @@ -179,21 +179,27 @@ const useUploadStatusQuery = ({ onStatusChange, onSuccess, }: { - onStatusChange: (status: JobStatus) => void - onSuccess: () => void + onStatusChange: (status: AppBskyVideoDefs.JobStatus) => void + onSuccess: (blobRef: BlobRef) => void }) => { + const videoAgent = useVideoAgent() const [enabled, setEnabled] = React.useState(true) const [jobId, setJobId] = React.useState<string>() const {isLoading, isError} = useQuery({ - queryKey: ['video-upload'], + queryKey: ['video-upload', jobId], queryFn: async () => { - const url = createVideoEndpointUrl(`/job/${jobId}/status`) - const res = await fetch(url) - const status = (await res.json()) as JobStatus - if (status.state === JobState.JOB_STATE_COMPLETED) { + if (!jobId) return // this won't happen, can ignore + + const {data} = await videoAgent.app.bsky.video.getJobStatus({jobId}) + const status = data.jobStatus + if (status.state === 'JOB_STATE_COMPLETED') { setEnabled(false) - onSuccess() + if (!status.blob) + throw new Error('Job completed, but did not return a blob') + onSuccess(status.blob) + } else if (status.state === 'JOB_STATE_FAILED') { + throw new Error('Job failed to process') } onStatusChange(status) return status |