APP-2670: refresh upload auth and track transport
This commit is contained in:
@@ -5,7 +5,10 @@
|
|||||||
import {type Platform} from 'react-native'
|
import {type Platform} from 'react-native'
|
||||||
|
|
||||||
import {type NotificationReason} from '#/lib/hooks/useNotificationHandler'
|
import {type NotificationReason} from '#/lib/hooks/useNotificationHandler'
|
||||||
import {type VideoCompressSkipReason} from '#/lib/media/video/types'
|
import {
|
||||||
|
type VideoCompressSkipReason,
|
||||||
|
type VideoUploadTransport,
|
||||||
|
} from '#/lib/media/video/types'
|
||||||
import {type NotificationType} from '#/state/queries/notifications/types'
|
import {type NotificationType} from '#/state/queries/notifications/types'
|
||||||
import {type FeedDescriptor} from '#/state/queries/post-feed'
|
import {type FeedDescriptor} from '#/state/queries/post-feed'
|
||||||
import {type LiveEventFeedMetricContext} from '#/features/liveEvents/types'
|
import {type LiveEventFeedMetricContext} from '#/features/liveEvents/types'
|
||||||
@@ -1420,6 +1423,7 @@ export type Events = {
|
|||||||
bytes: number
|
bytes: number
|
||||||
elapsedMs: number
|
elapsedMs: number
|
||||||
throughputBytesPerSec: number
|
throughputBytesPerSec: number
|
||||||
|
transport: VideoUploadTransport
|
||||||
}
|
}
|
||||||
'video:upload:uploadFailed': {
|
'video:upload:uploadFailed': {
|
||||||
uploadId: string
|
uploadId: string
|
||||||
@@ -1427,6 +1431,7 @@ export type Events = {
|
|||||||
bytes: number
|
bytes: number
|
||||||
errorClass: string
|
errorClass: string
|
||||||
elapsedMs: number
|
elapsedMs: number
|
||||||
|
transport: VideoUploadTransport
|
||||||
}
|
}
|
||||||
'video:upload:processingStarted': {
|
'video:upload:processingStarted': {
|
||||||
uploadId: string
|
uploadId: string
|
||||||
|
|||||||
@@ -26,14 +26,17 @@ export async function uploadVideoMultipart({
|
|||||||
agent,
|
agent,
|
||||||
setProgress,
|
setProgress,
|
||||||
signal,
|
signal,
|
||||||
|
onStarted,
|
||||||
}: {
|
}: {
|
||||||
video: CompressedVideo
|
video: CompressedVideo
|
||||||
agent: AtpAgent
|
agent: AtpAgent
|
||||||
setProgress: (progress: number) => void
|
setProgress: (progress: number) => void
|
||||||
signal: AbortSignal
|
signal: AbortSignal
|
||||||
|
onStarted?: () => void
|
||||||
}): Promise<AppBskyVideoDefs.JobStatus> {
|
}): Promise<AppBskyVideoDefs.JobStatus> {
|
||||||
throwIfAborted(signal)
|
throwIfAborted(signal)
|
||||||
let token = await mintToken(agent)
|
const tokenProvider = createTokenProvider(agent)
|
||||||
|
const token = await tokenProvider.get()
|
||||||
const name = `${nanoid(12)}.${mimeToExt(video.mimeType)}`
|
const name = `${nanoid(12)}.${mimeToExt(video.mimeType)}`
|
||||||
let session
|
let session
|
||||||
try {
|
try {
|
||||||
@@ -46,10 +49,14 @@ export async function uploadVideoMultipart({
|
|||||||
err instanceof Error ? err.message : 'Multipart upload unavailable',
|
err instanceof Error ? err.message : 'Multipart upload unavailable',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
onStarted?.()
|
||||||
|
|
||||||
const {jobId} = session
|
const {jobId} = session
|
||||||
const abortOnCancel = () => {
|
const abortOnCancel = () => {
|
||||||
void abortUpload(jobId, token).catch(() => {})
|
void tokenProvider
|
||||||
|
.get()
|
||||||
|
.then(currentToken => abortUpload(jobId, currentToken))
|
||||||
|
.catch(() => {})
|
||||||
}
|
}
|
||||||
signal.addEventListener('abort', abortOnCancel, {once: true})
|
signal.addEventListener('abort', abortOnCancel, {once: true})
|
||||||
let reader: ReturnType<typeof createChunkReader> | undefined
|
let reader: ReturnType<typeof createChunkReader> | undefined
|
||||||
@@ -64,24 +71,28 @@ export async function uploadVideoMultipart({
|
|||||||
await uploadParts({
|
await uploadParts({
|
||||||
parts,
|
parts,
|
||||||
reader,
|
reader,
|
||||||
uploadPart: createUploadPart(jobId, token),
|
uploadPart: createUploadPart(jobId, tokenProvider.get),
|
||||||
totalBytes: video.size,
|
totalBytes: video.size,
|
||||||
setProgress,
|
setProgress,
|
||||||
signal,
|
signal,
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (signal.aborted) throw new AbortError()
|
if (signal.aborted) throw new AbortError()
|
||||||
return await abortThenFallbackOrResolve(jobId, token, err)
|
return await abortThenFallbackOrResolve(
|
||||||
|
jobId,
|
||||||
|
await tokenProvider.get(),
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finish stores this credential for the later PDS blob upload, so use a
|
// Finish stores this credential for the later PDS blob upload, so use a
|
||||||
// fresh token rather than the one that may have aged during transfer.
|
// fresh token rather than the one that may have aged during transfer.
|
||||||
token = await mintToken(agent)
|
await tokenProvider.get(true)
|
||||||
const activeReader = reader
|
const activeReader = reader
|
||||||
if (!activeReader) throw new Error('Video chunk reader is unavailable')
|
if (!activeReader) throw new Error('Video chunk reader is unavailable')
|
||||||
return await finishAndRecover({
|
return await finishAndRecover({
|
||||||
jobId,
|
jobId,
|
||||||
token,
|
getToken: tokenProvider.get,
|
||||||
signal,
|
signal,
|
||||||
resendMissingParts: async receivedPartNumbers => {
|
resendMissingParts: async receivedPartNumbers => {
|
||||||
const missing = getMissingParts(parts, receivedPartNumbers)
|
const missing = getMissingParts(parts, receivedPartNumbers)
|
||||||
@@ -91,7 +102,7 @@ export async function uploadVideoMultipart({
|
|||||||
await uploadParts({
|
await uploadParts({
|
||||||
parts: missing,
|
parts: missing,
|
||||||
reader: activeReader,
|
reader: activeReader,
|
||||||
uploadPart: createUploadPart(jobId, token),
|
uploadPart: createUploadPart(jobId, tokenProvider.get),
|
||||||
totalBytes: missingBytes,
|
totalBytes: missingBytes,
|
||||||
setProgress: progress =>
|
setProgress: progress =>
|
||||||
setProgress(
|
setProgress(
|
||||||
@@ -110,18 +121,19 @@ export async function uploadVideoMultipart({
|
|||||||
|
|
||||||
async function finishAndRecover({
|
async function finishAndRecover({
|
||||||
jobId,
|
jobId,
|
||||||
token,
|
getToken,
|
||||||
signal,
|
signal,
|
||||||
resendMissingParts,
|
resendMissingParts,
|
||||||
}: {
|
}: {
|
||||||
jobId: string
|
jobId: string
|
||||||
token: string
|
getToken: () => Promise<string>
|
||||||
signal: AbortSignal
|
signal: AbortSignal
|
||||||
resendMissingParts: (receivedPartNumbers: number[]) => Promise<boolean>
|
resendMissingParts: (receivedPartNumbers: number[]) => Promise<boolean>
|
||||||
}): Promise<AppBskyVideoDefs.JobStatus> {
|
}): Promise<AppBskyVideoDefs.JobStatus> {
|
||||||
let createdFailures = 0
|
let createdFailures = 0
|
||||||
while (true) {
|
while (true) {
|
||||||
throwIfAborted(signal)
|
throwIfAborted(signal)
|
||||||
|
const token = await getToken()
|
||||||
try {
|
try {
|
||||||
const result = await finishUpload(jobId, token, signal)
|
const result = await finishUpload(jobId, token, signal)
|
||||||
return result.jobStatus
|
return result.jobStatus
|
||||||
@@ -221,12 +233,33 @@ async function abortThenFallbackOrResolve(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function mintToken(agent: AtpAgent) {
|
function createTokenProvider(agent: AtpAgent) {
|
||||||
return getServiceAuthToken({
|
let token: string | undefined
|
||||||
agent,
|
let expiresAt = 0
|
||||||
lxm: 'com.atproto.repo.uploadBlob',
|
let refresh: Promise<string> | undefined
|
||||||
exp: Date.now() / 1000 + 60 * 30,
|
|
||||||
})
|
async function get(forceRefresh = false) {
|
||||||
|
if (!forceRefresh && token && Date.now() < expiresAt - 60_000) return token
|
||||||
|
if (!refresh) {
|
||||||
|
const exp = Math.floor(Date.now() / 1000) + 60 * 30
|
||||||
|
refresh = getServiceAuthToken({
|
||||||
|
agent,
|
||||||
|
lxm: 'com.atproto.repo.uploadBlob',
|
||||||
|
exp,
|
||||||
|
})
|
||||||
|
.then(nextToken => {
|
||||||
|
token = nextToken
|
||||||
|
expiresAt = exp * 1000
|
||||||
|
return nextToken
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
refresh = undefined
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return refresh
|
||||||
|
}
|
||||||
|
|
||||||
|
return {get}
|
||||||
}
|
}
|
||||||
|
|
||||||
function throwIfAborted(signal: AbortSignal) {
|
function throwIfAborted(signal: AbortSignal) {
|
||||||
|
|||||||
@@ -3,69 +3,92 @@ import {createVideoEndpointUrl} from '#/lib/media/video/util'
|
|||||||
import {MultipartUploadError} from './api'
|
import {MultipartUploadError} from './api'
|
||||||
import {type UploadPartFn} from './types'
|
import {type UploadPartFn} from './types'
|
||||||
|
|
||||||
export function createUploadPart(jobId: string, token: string): UploadPartFn {
|
export function createUploadPart(
|
||||||
return ({part, chunk, onProgress, signal}) =>
|
jobId: string,
|
||||||
new Promise((resolve, reject) => {
|
getToken: (forceRefresh?: boolean) => Promise<string>,
|
||||||
if (signal.aborted) {
|
): UploadPartFn {
|
||||||
reject(new AbortError())
|
return async args => {
|
||||||
return
|
try {
|
||||||
|
return await sendPart(jobId, await getToken(), args)
|
||||||
|
} catch (err) {
|
||||||
|
if (
|
||||||
|
err instanceof MultipartUploadError &&
|
||||||
|
(err.status === 401 || err.error === 'AuthRequired')
|
||||||
|
) {
|
||||||
|
args.onProgress(0)
|
||||||
|
return await sendPart(jobId, await getToken(true), args)
|
||||||
}
|
}
|
||||||
const xhr = new XMLHttpRequest()
|
throw err
|
||||||
const abort = () => xhr.abort()
|
}
|
||||||
signal.addEventListener('abort', abort, {once: true})
|
}
|
||||||
const cleanup = () => signal.removeEventListener('abort', abort)
|
}
|
||||||
|
|
||||||
xhr.upload.addEventListener('progress', event => {
|
function sendPart(
|
||||||
onProgress(event.loaded)
|
jobId: string,
|
||||||
})
|
token: string,
|
||||||
xhr.onerror = () => {
|
{part, chunk, onProgress, signal}: Parameters<UploadPartFn>[0],
|
||||||
cleanup()
|
) {
|
||||||
reject(new TypeError('Network request failed'))
|
return new Promise<Awaited<ReturnType<UploadPartFn>>>((resolve, reject) => {
|
||||||
}
|
if (signal.aborted) {
|
||||||
xhr.onabort = () => {
|
reject(new AbortError())
|
||||||
cleanup()
|
return
|
||||||
reject(new AbortError())
|
}
|
||||||
}
|
const xhr = new XMLHttpRequest()
|
||||||
xhr.onload = () => {
|
const abort = () => xhr.abort()
|
||||||
cleanup()
|
signal.addEventListener('abort', abort, {once: true})
|
||||||
let data: {
|
const cleanup = () => signal.removeEventListener('abort', abort)
|
||||||
partNumber?: number
|
|
||||||
sizeBytes?: number
|
xhr.upload.addEventListener('progress', event => {
|
||||||
error?: string
|
onProgress(event.loaded)
|
||||||
message?: string
|
})
|
||||||
}
|
xhr.onerror = () => {
|
||||||
try {
|
cleanup()
|
||||||
data = JSON.parse(xhr.responseText)
|
reject(new TypeError('Network request failed'))
|
||||||
} catch {
|
}
|
||||||
data = {}
|
xhr.onabort = () => {
|
||||||
}
|
cleanup()
|
||||||
if (xhr.status < 200 || xhr.status >= 300) {
|
reject(new AbortError())
|
||||||
reject(
|
}
|
||||||
new MultipartUploadError(
|
xhr.onload = () => {
|
||||||
data.message ||
|
cleanup()
|
||||||
data.error ||
|
let data: {
|
||||||
`Video service returned ${xhr.status}`,
|
partNumber?: number
|
||||||
data.error,
|
sizeBytes?: number
|
||||||
xhr.status,
|
error?: string
|
||||||
),
|
message?: string
|
||||||
)
|
}
|
||||||
} else {
|
try {
|
||||||
onProgress(part.size)
|
data = JSON.parse(xhr.responseText)
|
||||||
resolve({
|
} catch {
|
||||||
partNumber: data.partNumber ?? part.partNumber,
|
data = {}
|
||||||
sizeBytes: data.sizeBytes ?? part.size,
|
}
|
||||||
})
|
if (xhr.status < 200 || xhr.status >= 300) {
|
||||||
}
|
reject(
|
||||||
}
|
new MultipartUploadError(
|
||||||
xhr.open(
|
data.message ||
|
||||||
'POST',
|
data.error ||
|
||||||
createVideoEndpointUrl('/xrpc/app.bsky.video.uploadPart', {
|
`Video service returned ${xhr.status}`,
|
||||||
jobId,
|
data.error,
|
||||||
partNumber: String(part.partNumber),
|
xhr.status,
|
||||||
}),
|
),
|
||||||
)
|
)
|
||||||
xhr.setRequestHeader('Content-Type', 'application/octet-stream')
|
} else {
|
||||||
xhr.setRequestHeader('Authorization', `Bearer ${token}`)
|
onProgress(part.size)
|
||||||
xhr.send(chunk as XMLHttpRequestBodyInit)
|
resolve({
|
||||||
})
|
partNumber: data.partNumber ?? part.partNumber,
|
||||||
|
sizeBytes: data.sizeBytes ?? part.size,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
xhr.open(
|
||||||
|
'POST',
|
||||||
|
createVideoEndpointUrl('/xrpc/app.bsky.video.uploadPart', {
|
||||||
|
jobId,
|
||||||
|
partNumber: String(part.partNumber),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
xhr.setRequestHeader('Content-Type', 'application/octet-stream')
|
||||||
|
xhr.setRequestHeader('Authorization', `Bearer ${token}`)
|
||||||
|
xhr.send(chunk as XMLHttpRequestBodyInit)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {nanoid} from 'nanoid/non-secure'
|
|||||||
import {
|
import {
|
||||||
type ProbedMetadata,
|
type ProbedMetadata,
|
||||||
type VideoCompressSkipReason,
|
type VideoCompressSkipReason,
|
||||||
|
type VideoUploadTransport,
|
||||||
} from '#/lib/media/video/types'
|
} from '#/lib/media/video/types'
|
||||||
import {Sentry} from '#/logger/sentry/lib'
|
import {Sentry} from '#/logger/sentry/lib'
|
||||||
import {type Metrics} from '#/analytics/metrics'
|
import {type Metrics} from '#/analytics/metrics'
|
||||||
@@ -45,6 +46,7 @@ export type VideoTelemetry = {
|
|||||||
compressCompleted: (video: {size: number; mimeType: string}) => void
|
compressCompleted: (video: {size: number; mimeType: string}) => void
|
||||||
compressFailed: (e: unknown) => void
|
compressFailed: (e: unknown) => void
|
||||||
uploadStarted: (bytes: number) => void
|
uploadStarted: (bytes: number) => void
|
||||||
|
uploadTransport: (transport: VideoUploadTransport) => void
|
||||||
uploadCompleted: (jobId: string) => void
|
uploadCompleted: (jobId: string) => void
|
||||||
uploadFailed: (e: unknown) => void
|
uploadFailed: (e: unknown) => void
|
||||||
processingStarted: (jobId: string) => void
|
processingStarted: (jobId: string) => void
|
||||||
@@ -70,6 +72,7 @@ export function createVideoTelemetry({
|
|||||||
let phaseStartedAt = startedAt
|
let phaseStartedAt = startedAt
|
||||||
let jobId: string | undefined
|
let jobId: string | undefined
|
||||||
let uploadBytes: number | undefined
|
let uploadBytes: number | undefined
|
||||||
|
let uploadTransport: VideoUploadTransport = 'legacy'
|
||||||
let txnEnded = false
|
let txnEnded = false
|
||||||
let abortBound = true
|
let abortBound = true
|
||||||
|
|
||||||
@@ -226,6 +229,11 @@ export function createVideoTelemetry({
|
|||||||
metric('video:upload:uploadStarted', {uploadId, engine, bytes})
|
metric('video:upload:uploadStarted', {uploadId, engine, bytes})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
uploadTransport(transport) {
|
||||||
|
uploadTransport = transport
|
||||||
|
phaseSpan?.setAttribute('video.upload.transport', transport)
|
||||||
|
},
|
||||||
|
|
||||||
uploadCompleted(id) {
|
uploadCompleted(id) {
|
||||||
jobId = id
|
jobId = id
|
||||||
const elapsedMs = Date.now() - phaseStartedAt
|
const elapsedMs = Date.now() - phaseStartedAt
|
||||||
@@ -238,6 +246,7 @@ export function createVideoTelemetry({
|
|||||||
elapsedMs,
|
elapsedMs,
|
||||||
throughputBytesPerSec:
|
throughputBytesPerSec:
|
||||||
elapsedMs > 0 ? Math.round((bytes * 1000) / elapsedMs) : 0,
|
elapsedMs > 0 ? Math.round((bytes * 1000) / elapsedMs) : 0,
|
||||||
|
transport: uploadTransport,
|
||||||
})
|
})
|
||||||
endPhaseSpan()
|
endPhaseSpan()
|
||||||
phase = undefined
|
phase = undefined
|
||||||
@@ -250,6 +259,7 @@ export function createVideoTelemetry({
|
|||||||
bytes: uploadBytes ?? 0,
|
bytes: uploadBytes ?? 0,
|
||||||
errorClass: errorClass(e),
|
errorClass: errorClass(e),
|
||||||
elapsedMs: Date.now() - phaseStartedAt,
|
elapsedMs: Date.now() - phaseStartedAt,
|
||||||
|
transport: uploadTransport,
|
||||||
})
|
})
|
||||||
endTxn('error')
|
endTxn('error')
|
||||||
detachAbort()
|
detachAbort()
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ export type VideoCompressSkipReason =
|
|||||||
| 'no-webcodecs'
|
| 'no-webcodecs'
|
||||||
| 'compress-error-fallback'
|
| 'compress-error-fallback'
|
||||||
|
|
||||||
|
export type VideoUploadTransport = 'multipart' | 'legacy' | 'legacy-fallback'
|
||||||
|
|
||||||
export type CompressedVideo = {
|
export type CompressedVideo = {
|
||||||
uri: string
|
uri: string
|
||||||
mimeType: string
|
mimeType: string
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import {nanoid} from 'nanoid/non-secure'
|
|||||||
|
|
||||||
import {AbortError} from '#/lib/async/cancelable'
|
import {AbortError} from '#/lib/async/cancelable'
|
||||||
import {ServerError} from '#/lib/media/video/errors'
|
import {ServerError} from '#/lib/media/video/errors'
|
||||||
import {type CompressedVideo} from '#/lib/media/video/types'
|
import {
|
||||||
|
type CompressedVideo,
|
||||||
|
type VideoUploadTransport,
|
||||||
|
} from '#/lib/media/video/types'
|
||||||
import {Features, features} from '#/analytics/features'
|
import {Features, features} from '#/analytics/features'
|
||||||
import {MultipartFallbackError, uploadVideoMultipart} from './multipart/upload'
|
import {MultipartFallbackError, uploadVideoMultipart} from './multipart/upload'
|
||||||
import {getServiceAuthToken, getVideoUploadLimits} from './upload.shared'
|
import {getServiceAuthToken, getVideoUploadLimits} from './upload.shared'
|
||||||
@@ -19,6 +22,7 @@ export async function uploadVideo({
|
|||||||
setProgress,
|
setProgress,
|
||||||
signal,
|
signal,
|
||||||
i18n,
|
i18n,
|
||||||
|
onTransport,
|
||||||
}: {
|
}: {
|
||||||
video: CompressedVideo
|
video: CompressedVideo
|
||||||
agent: AtpAgent
|
agent: AtpAgent
|
||||||
@@ -26,6 +30,7 @@ export async function uploadVideo({
|
|||||||
setProgress: (progress: number) => void
|
setProgress: (progress: number) => void
|
||||||
signal: AbortSignal
|
signal: AbortSignal
|
||||||
i18n: I18n
|
i18n: I18n
|
||||||
|
onTransport?: (transport: VideoUploadTransport) => void
|
||||||
}) {
|
}) {
|
||||||
if (signal.aborted) {
|
if (signal.aborted) {
|
||||||
throw new AbortError()
|
throw new AbortError()
|
||||||
@@ -34,11 +39,20 @@ export async function uploadVideo({
|
|||||||
|
|
||||||
if (features.isOn(Features.VideoMultipartUploadEnable)) {
|
if (features.isOn(Features.VideoMultipartUploadEnable)) {
|
||||||
try {
|
try {
|
||||||
return await uploadVideoMultipart({video, agent, setProgress, signal})
|
return await uploadVideoMultipart({
|
||||||
|
video,
|
||||||
|
agent,
|
||||||
|
setProgress,
|
||||||
|
signal,
|
||||||
|
onStarted: () => onTransport?.('multipart'),
|
||||||
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!(err instanceof MultipartFallbackError)) throw err
|
if (!(err instanceof MultipartFallbackError)) throw err
|
||||||
|
onTransport?.('legacy-fallback')
|
||||||
setProgress(0)
|
setProgress(0)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
onTransport?.('legacy')
|
||||||
}
|
}
|
||||||
|
|
||||||
const uri = createVideoEndpointUrl('/xrpc/app.bsky.video.uploadVideo', {
|
const uri = createVideoEndpointUrl('/xrpc/app.bsky.video.uploadVideo', {
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import {nanoid} from 'nanoid/non-secure'
|
|||||||
|
|
||||||
import {AbortError} from '#/lib/async/cancelable'
|
import {AbortError} from '#/lib/async/cancelable'
|
||||||
import {ServerError} from '#/lib/media/video/errors'
|
import {ServerError} from '#/lib/media/video/errors'
|
||||||
import {type CompressedVideo} from '#/lib/media/video/types'
|
import {
|
||||||
|
type CompressedVideo,
|
||||||
|
type VideoUploadTransport,
|
||||||
|
} from '#/lib/media/video/types'
|
||||||
import {Features, features} from '#/analytics/features'
|
import {Features, features} from '#/analytics/features'
|
||||||
import {MultipartFallbackError, uploadVideoMultipart} from './multipart/upload'
|
import {MultipartFallbackError, uploadVideoMultipart} from './multipart/upload'
|
||||||
import {getServiceAuthToken, getVideoUploadLimits} from './upload.shared'
|
import {getServiceAuthToken, getVideoUploadLimits} from './upload.shared'
|
||||||
@@ -18,6 +21,7 @@ export async function uploadVideo({
|
|||||||
setProgress,
|
setProgress,
|
||||||
signal,
|
signal,
|
||||||
i18n,
|
i18n,
|
||||||
|
onTransport,
|
||||||
}: {
|
}: {
|
||||||
video: CompressedVideo
|
video: CompressedVideo
|
||||||
agent: AtpAgent
|
agent: AtpAgent
|
||||||
@@ -25,6 +29,7 @@ export async function uploadVideo({
|
|||||||
setProgress: (progress: number) => void
|
setProgress: (progress: number) => void
|
||||||
signal: AbortSignal
|
signal: AbortSignal
|
||||||
i18n: I18n
|
i18n: I18n
|
||||||
|
onTransport?: (transport: VideoUploadTransport) => void
|
||||||
}) {
|
}) {
|
||||||
if (signal.aborted) {
|
if (signal.aborted) {
|
||||||
throw new AbortError()
|
throw new AbortError()
|
||||||
@@ -33,11 +38,20 @@ export async function uploadVideo({
|
|||||||
|
|
||||||
if (features.isOn(Features.VideoMultipartUploadEnable)) {
|
if (features.isOn(Features.VideoMultipartUploadEnable)) {
|
||||||
try {
|
try {
|
||||||
return await uploadVideoMultipart({video, agent, setProgress, signal})
|
return await uploadVideoMultipart({
|
||||||
|
video,
|
||||||
|
agent,
|
||||||
|
setProgress,
|
||||||
|
signal,
|
||||||
|
onStarted: () => onTransport?.('multipart'),
|
||||||
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!(err instanceof MultipartFallbackError)) throw err
|
if (!(err instanceof MultipartFallbackError)) throw err
|
||||||
|
onTransport?.('legacy-fallback')
|
||||||
setProgress(0)
|
setProgress(0)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
onTransport?.('legacy')
|
||||||
}
|
}
|
||||||
|
|
||||||
const uri = createVideoEndpointUrl('/xrpc/app.bsky.video.uploadVideo', {
|
const uri = createVideoEndpointUrl('/xrpc/app.bsky.video.uploadVideo', {
|
||||||
|
|||||||
@@ -326,6 +326,7 @@ export async function processVideo(
|
|||||||
did,
|
did,
|
||||||
signal,
|
signal,
|
||||||
i18n,
|
i18n,
|
||||||
|
onTransport: telemetry.uploadTransport,
|
||||||
setProgress: p => {
|
setProgress: p => {
|
||||||
dispatch({type: 'update_progress', progress: p, signal})
|
dispatch({type: 'update_progress', progress: p, signal})
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user