f02b9e3eeb
Co-authored-by: Claude <noreply@anthropic.com>
130 lines
3.3 KiB
TypeScript
130 lines
3.3 KiB
TypeScript
import {type Client} from '@atproto/lex'
|
|
import {type I18n} from '@lingui/core'
|
|
import {msg} from '@lingui/core/macro'
|
|
import {nanoid} from 'nanoid/non-secure'
|
|
|
|
import {AbortError} from '#/lib/async/cancelable'
|
|
import {ServerError} from '#/lib/media/video/errors'
|
|
import {
|
|
type CompressedVideo,
|
|
type VideoUploadTransport,
|
|
} from '#/lib/media/video/types'
|
|
import {Features, features} from '#/analytics/features'
|
|
import {type app} from '#/lexicons'
|
|
import {MultipartFallbackError, uploadVideoMultipart} from './multipart/upload'
|
|
import {
|
|
getServiceAuthToken,
|
|
getVideoUploadLimits,
|
|
serviceAuthExp,
|
|
} from './upload.shared'
|
|
import {createVideoEndpointUrl, mimeToExt} from './util'
|
|
|
|
export async function uploadVideo({
|
|
video,
|
|
client,
|
|
dispatchUrl,
|
|
did,
|
|
setProgress,
|
|
signal,
|
|
i18n,
|
|
onTransport,
|
|
}: {
|
|
video: CompressedVideo
|
|
client: Client
|
|
/** The account's PDS/dispatch URL, for the uploadBlob service-auth token. */
|
|
dispatchUrl: string | URL
|
|
did: string
|
|
setProgress: (progress: number) => void
|
|
signal: AbortSignal
|
|
i18n: I18n
|
|
onTransport?: (transport: VideoUploadTransport) => void
|
|
}) {
|
|
if (signal.aborted) {
|
|
throw new AbortError()
|
|
}
|
|
await getVideoUploadLimits(client, i18n)
|
|
|
|
if (features.isOn(Features.VideoMultipartUploadEnable)) {
|
|
try {
|
|
return await uploadVideoMultipart({
|
|
video,
|
|
client,
|
|
dispatchUrl,
|
|
setProgress,
|
|
signal,
|
|
onStarted: () => onTransport?.('multipart'),
|
|
})
|
|
} catch (err) {
|
|
if (!(err instanceof MultipartFallbackError)) throw err
|
|
onTransport?.('legacy-fallback')
|
|
setProgress(0)
|
|
}
|
|
} else {
|
|
onTransport?.('legacy')
|
|
}
|
|
|
|
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({
|
|
client,
|
|
dispatchUrl,
|
|
lxm: 'com.atproto.repo.uploadBlob',
|
|
exp: serviceAuthExp(),
|
|
})
|
|
|
|
if (signal.aborted) {
|
|
throw new AbortError()
|
|
}
|
|
const xhr = new XMLHttpRequest()
|
|
const res = await new Promise<app.bsky.video.defs.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 app.bsky.video.defs.JobStatus
|
|
resolve(uploadRes)
|
|
} else {
|
|
reject(new ServerError(i18n._(msg`Failed to upload video`)))
|
|
}
|
|
}
|
|
xhr.onerror = () => {
|
|
reject(new ServerError(i18n._(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 || i18n._(msg`Failed to upload video`))
|
|
}
|
|
|
|
if (signal.aborted) {
|
|
throw new AbortError()
|
|
}
|
|
return res
|
|
}
|