diff options
author | dan <dan.abramov@gmail.com> | 2024-10-03 14:57:48 +0900 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-10-03 14:57:48 +0900 |
commit | 2aa365b5b6fed920d17d307252a7af7c52b95855 (patch) | |
tree | b8083438359e63367e983b461f5835cf8b75aa8c /src/lib/media | |
parent | 03704e2b48e6cdc348ce7277f2bcae0c61519d1e (diff) | |
download | voidsky-2aa365b5b6fed920d17d307252a7af7c52b95855.tar.zst |
Rename some files and variables (#5587)
* Move composer reducers together * videoUploadState -> videoState * Inline videoDispatch
Diffstat (limited to 'src/lib/media')
-rw-r--r-- | src/lib/media/video/compress.ts | 2 | ||||
-rw-r--r-- | src/lib/media/video/upload.shared.ts | 61 | ||||
-rw-r--r-- | src/lib/media/video/upload.ts | 79 | ||||
-rw-r--r-- | src/lib/media/video/upload.web.ts | 95 | ||||
-rw-r--r-- | src/lib/media/video/util.ts | 53 |
5 files changed, 289 insertions, 1 deletions
diff --git a/src/lib/media/video/compress.ts b/src/lib/media/video/compress.ts index dec9032a3..c2d1470c6 100644 --- a/src/lib/media/video/compress.ts +++ b/src/lib/media/video/compress.ts @@ -2,8 +2,8 @@ import {getVideoMetaData, Video} from 'react-native-compressor' import {ImagePickerAsset} from 'expo-image-picker' import {SUPPORTED_MIME_TYPES, SupportedMimeTypes} from '#/lib/constants' -import {extToMime} from '#/state/queries/video/util' import {CompressedVideo} from './types' +import {extToMime} from './util' const MIN_SIZE_FOR_COMPRESSION = 25 // 25mb diff --git a/src/lib/media/video/upload.shared.ts b/src/lib/media/video/upload.shared.ts new file mode 100644 index 000000000..8c217eadc --- /dev/null +++ b/src/lib/media/video/upload.shared.ts @@ -0,0 +1,61 @@ +import {BskyAgent} from '@atproto/api' +import {I18n} from '@lingui/core' +import {msg} from '@lingui/macro' + +import {VIDEO_SERVICE_DID} from '#/lib/constants' +import {UploadLimitError} from '#/lib/media/video/errors' +import {getServiceAuthAudFromUrl} from '#/lib/strings/url-helpers' +import {createVideoAgent} from './util' + +export async function getServiceAuthToken({ + agent, + aud, + lxm, + exp, +}: { + agent: BskyAgent + aud?: string + lxm: string + exp?: number +}) { + const pdsAud = getServiceAuthAudFromUrl(agent.dispatchUrl) + if (!pdsAud) { + throw new Error('Agent does not have a PDS URL') + } + const {data: serviceAuth} = await agent.com.atproto.server.getServiceAuth({ + aud: aud ?? pdsAud, + lxm, + exp, + }) + return serviceAuth.token +} + +export async function getVideoUploadLimits(agent: BskyAgent, _: I18n['_']) { + const token = await getServiceAuthToken({ + agent, + lxm: 'app.bsky.video.getUploadLimits', + aud: VIDEO_SERVICE_DID, + }) + const videoAgent = createVideoAgent() + const {data: limits} = await videoAgent.app.bsky.video + .getUploadLimits({}, {headers: {Authorization: `Bearer ${token}`}}) + .catch(err => { + if (err instanceof Error) { + throw new UploadLimitError(err.message) + } else { + throw err + } + }) + + if (!limits.canUpload) { + if (limits.message) { + throw new UploadLimitError(limits.message) + } else { + throw new UploadLimitError( + _( + msg`You have temporarily reached the limit for video uploads. Please try again later.`, + ), + ) + } + } +} diff --git a/src/lib/media/video/upload.ts b/src/lib/media/video/upload.ts new file mode 100644 index 000000000..3330370b3 --- /dev/null +++ b/src/lib/media/video/upload.ts @@ -0,0 +1,79 @@ +import {createUploadTask, FileSystemUploadType} from 'expo-file-system' +import {AppBskyVideoDefs, BskyAgent} from '@atproto/api' +import {I18n} from '@lingui/core' +import {msg} from '@lingui/macro' +import {nanoid} from 'nanoid/non-secure' + +import {AbortError} from '#/lib/async/cancelable' +import {ServerError} from '#/lib/media/video/errors' +import {CompressedVideo} from '#/lib/media/video/types' +import {createVideoEndpointUrl, mimeToExt} from './util' +import {getServiceAuthToken, getVideoUploadLimits} from './upload.shared' + +export async function uploadVideo({ + video, + agent, + did, + setProgress, + signal, + _, +}: { + video: CompressedVideo + agent: BskyAgent + did: string + setProgress: (progress: number) => void + signal: AbortSignal + _: I18n['_'] +}) { + if (signal.aborted) { + throw new AbortError() + } + await getVideoUploadLimits(agent, _) + + const uri = createVideoEndpointUrl('/xrpc/app.bsky.video.uploadVideo', { + did, + name: `${nanoid(12)}.${mimeToExt(video.mimeType)}`, + }) + + if (signal.aborted) { + throw new AbortError() + } + const token = await getServiceAuthToken({ + agent, + lxm: 'com.atproto.repo.uploadBlob', + exp: Date.now() / 1000 + 60 * 30, // 30 minutes + }) + const uploadTask = createUploadTask( + uri, + video.uri, + { + headers: { + 'content-type': video.mimeType, + Authorization: `Bearer ${token}`, + }, + httpMethod: 'POST', + uploadType: FileSystemUploadType.BINARY_CONTENT, + }, + p => setProgress(p.totalBytesSent / p.totalBytesExpectedToSend), + ) + + if (signal.aborted) { + throw new AbortError() + } + const res = await uploadTask.uploadAsync() + + if (!res?.body) { + throw new Error('No response') + } + + const responseBody = JSON.parse(res.body) as AppBskyVideoDefs.JobStatus + + if (!responseBody.jobId) { + throw new ServerError(responseBody.error || _(msg`Failed to upload video`)) + } + + if (signal.aborted) { + throw new AbortError() + } + return responseBody +} diff --git a/src/lib/media/video/upload.web.ts b/src/lib/media/video/upload.web.ts new file mode 100644 index 000000000..ec65f96c9 --- /dev/null +++ b/src/lib/media/video/upload.web.ts @@ -0,0 +1,95 @@ +import {AppBskyVideoDefs} from '@atproto/api' +import {BskyAgent} from '@atproto/api' +import {I18n} from '@lingui/core' +import {msg} from '@lingui/macro' +import {nanoid} from 'nanoid/non-secure' + +import {AbortError} from '#/lib/async/cancelable' +import {ServerError} from '#/lib/media/video/errors' +import {CompressedVideo} from '#/lib/media/video/types' +import {createVideoEndpointUrl, mimeToExt} from './util' +import {getServiceAuthToken, getVideoUploadLimits} from './upload.shared' + +export async function uploadVideo({ + video, + agent, + did, + setProgress, + signal, + _, +}: { + video: CompressedVideo + agent: BskyAgent + did: string + setProgress: (progress: number) => void + signal: AbortSignal + _: I18n['_'] +}) { + if (signal.aborted) { + throw new AbortError() + } + await getVideoUploadLimits(agent, _) + + const uri = createVideoEndpointUrl('/xrpc/app.bsky.video.uploadVideo', { + did, + name: `${nanoid(12)}.${mimeToExt(video.mimeType)}`, + }) + + let bytes = video.bytes + if (!bytes) { + if (signal.aborted) { + throw new AbortError() + } + bytes = await fetch(video.uri).then(res => res.arrayBuffer()) + } + + if (signal.aborted) { + throw new AbortError() + } + const token = await getServiceAuthToken({ + agent, + lxm: 'com.atproto.repo.uploadBlob', + exp: Date.now() / 1000 + 60 * 30, // 30 minutes + }) + + if (signal.aborted) { + throw new AbortError() + } + const xhr = new XMLHttpRequest() + 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 (signal.aborted) { + reject(new AbortError()) + } else if (xhr.readyState === 4) { + const uploadRes = JSON.parse( + xhr.responseText, + ) as AppBskyVideoDefs.JobStatus + resolve(uploadRes) + } else { + reject(new ServerError(_(msg`Failed to upload video`))) + } + } + xhr.onerror = () => { + reject(new ServerError(_(msg`Failed to upload video`))) + } + xhr.open('POST', uri) + xhr.setRequestHeader('Content-Type', video.mimeType) + xhr.setRequestHeader('Authorization', `Bearer ${token}`) + xhr.send(bytes) + }, + ) + + if (!res.jobId) { + throw new ServerError(res.error || _(msg`Failed to upload video`)) + } + + if (signal.aborted) { + throw new AbortError() + } + return res +} diff --git a/src/lib/media/video/util.ts b/src/lib/media/video/util.ts new file mode 100644 index 000000000..87b422c2c --- /dev/null +++ b/src/lib/media/video/util.ts @@ -0,0 +1,53 @@ +import {AtpAgent} from '@atproto/api' + +import {SupportedMimeTypes, VIDEO_SERVICE} from '#/lib/constants' + +export const createVideoEndpointUrl = ( + route: string, + params?: Record<string, string>, +) => { + const url = new URL(VIDEO_SERVICE) + url.pathname = route + if (params) { + for (const key in params) { + url.searchParams.set(key, params[key]) + } + } + return url.href +} + +export function createVideoAgent() { + return new AtpAgent({ + service: VIDEO_SERVICE, + }) +} + +export function mimeToExt(mimeType: SupportedMimeTypes | (string & {})) { + switch (mimeType) { + case 'video/mp4': + return 'mp4' + case 'video/webm': + return 'webm' + case 'video/mpeg': + return 'mpeg' + case 'video/quicktime': + return 'mov' + default: + throw new Error(`Unsupported mime type: ${mimeType}`) + } +} + +export function extToMime(ext: string) { + switch (ext) { + case 'mp4': + return 'video/mp4' + case 'webm': + return 'video/webm' + case 'mpeg': + return 'video/mpeg' + case 'mov': + return 'video/quicktime' + default: + throw new Error(`Unsupported file extension: ${ext}`) + } +} |