Improve video upload progress indicator
This commit is contained in:
committed by
Samuel Newman
parent
98fa8eafdf
commit
590abd05c9
@@ -2672,9 +2672,14 @@ function VideoUploadToolbar({state}: {state: VideoState}) {
|
|||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const progress = state.progress
|
const progress = state.progress
|
||||||
|
const processingProgress =
|
||||||
|
state.status === 'processing' ? state.jobStatus?.progress : undefined
|
||||||
const shouldRotate =
|
const shouldRotate =
|
||||||
state.status === 'processing' && (progress === 0 || progress === 1)
|
state.status === 'processing' &&
|
||||||
let wheelProgress = shouldRotate ? 0.33 : progress
|
(processingProgress === undefined ||
|
||||||
|
processingProgress <= 0 ||
|
||||||
|
processingProgress >= 100)
|
||||||
|
let wheelProgress = progress
|
||||||
|
|
||||||
const rotate = useDerivedValue(() => {
|
const rotate = useDerivedValue(() => {
|
||||||
if (shouldRotate) {
|
if (shouldRotate) {
|
||||||
@@ -2723,7 +2728,7 @@ function VideoUploadToolbar({state}: {state: VideoState}) {
|
|||||||
break
|
break
|
||||||
case 'error':
|
case 'error':
|
||||||
text = l`Error`
|
text = l`Error`
|
||||||
wheelProgress = 100
|
wheelProgress = 1
|
||||||
break
|
break
|
||||||
case 'done':
|
case 'done':
|
||||||
if (isGif) {
|
if (isGif) {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {uploadVideo} from '#/lib/media/video/upload'
|
|||||||
import {createVideoAgent} from '#/lib/media/video/util'
|
import {createVideoAgent} from '#/lib/media/video/util'
|
||||||
import {isNetworkError} from '#/lib/strings/errors'
|
import {isNetworkError} from '#/lib/strings/errors'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
import {advanceVideoProgress, videoProgressForPhase} from './videoProgress'
|
||||||
|
|
||||||
type CaptionsTrack = {lang: string; file: File}
|
type CaptionsTrack = {lang: string; file: File}
|
||||||
|
|
||||||
@@ -74,7 +75,7 @@ export type NoVideoState = typeof NO_VIDEO
|
|||||||
|
|
||||||
type ErrorState = {
|
type ErrorState = {
|
||||||
status: 'error'
|
status: 'error'
|
||||||
progress: 100
|
progress: number
|
||||||
abortController: AbortController
|
abortController: AbortController
|
||||||
asset: ImagePickerAsset | null
|
asset: ImagePickerAsset | null
|
||||||
video: CompressedVideo | null
|
video: CompressedVideo | null
|
||||||
@@ -128,7 +129,7 @@ type ProcessingState = {
|
|||||||
|
|
||||||
type DoneState = {
|
type DoneState = {
|
||||||
status: 'done'
|
status: 'done'
|
||||||
progress: 100
|
progress: 1
|
||||||
abortController: AbortController
|
abortController: AbortController
|
||||||
asset: ImagePickerAsset
|
asset: ImagePickerAsset
|
||||||
video: CompressedVideo
|
video: CompressedVideo
|
||||||
@@ -173,7 +174,7 @@ export function videoReducer(
|
|||||||
if (action.type === 'to_error') {
|
if (action.type === 'to_error') {
|
||||||
return {
|
return {
|
||||||
status: 'error',
|
status: 'error',
|
||||||
progress: 100,
|
progress: state.progress,
|
||||||
abortController: state.abortController,
|
abortController: state.abortController,
|
||||||
error: action.error,
|
error: action.error,
|
||||||
asset: state.asset ?? null,
|
asset: state.asset ?? null,
|
||||||
@@ -187,7 +188,11 @@ export function videoReducer(
|
|||||||
if (state.status === 'compressing' || state.status === 'uploading') {
|
if (state.status === 'compressing' || state.status === 'uploading') {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
progress: action.progress,
|
progress: advanceVideoProgress(
|
||||||
|
state.progress,
|
||||||
|
state.status,
|
||||||
|
action.progress,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (action.type === 'update_alt_text') {
|
} else if (action.type === 'update_alt_text') {
|
||||||
@@ -204,7 +209,7 @@ export function videoReducer(
|
|||||||
if (state.status === 'compressing') {
|
if (state.status === 'compressing') {
|
||||||
return {
|
return {
|
||||||
status: 'uploading',
|
status: 'uploading',
|
||||||
progress: 0,
|
progress: videoProgressForPhase('uploading', 0),
|
||||||
abortController: state.abortController,
|
abortController: state.abortController,
|
||||||
asset: state.asset,
|
asset: state.asset,
|
||||||
video: action.video,
|
video: action.video,
|
||||||
@@ -218,7 +223,7 @@ export function videoReducer(
|
|||||||
if (state.status === 'uploading') {
|
if (state.status === 'uploading') {
|
||||||
return {
|
return {
|
||||||
status: 'processing',
|
status: 'processing',
|
||||||
progress: 0,
|
progress: videoProgressForPhase('processing', 0),
|
||||||
abortController: state.abortController,
|
abortController: state.abortController,
|
||||||
asset: state.asset,
|
asset: state.asset,
|
||||||
video: state.video,
|
video: state.video,
|
||||||
@@ -236,7 +241,11 @@ export function videoReducer(
|
|||||||
jobStatus: action.jobStatus,
|
jobStatus: action.jobStatus,
|
||||||
progress:
|
progress:
|
||||||
action.jobStatus.progress !== undefined
|
action.jobStatus.progress !== undefined
|
||||||
? action.jobStatus.progress / 100
|
? advanceVideoProgress(
|
||||||
|
state.progress,
|
||||||
|
'processing',
|
||||||
|
action.jobStatus.progress / 100,
|
||||||
|
)
|
||||||
: state.progress,
|
: state.progress,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -244,7 +253,7 @@ export function videoReducer(
|
|||||||
if (state.status === 'processing') {
|
if (state.status === 'processing') {
|
||||||
return {
|
return {
|
||||||
status: 'done',
|
status: 'done',
|
||||||
progress: 100,
|
progress: 1,
|
||||||
abortController: state.abortController,
|
abortController: state.abortController,
|
||||||
asset: state.asset,
|
asset: state.asset,
|
||||||
video: state.video,
|
video: state.video,
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import {advanceVideoProgress, videoProgressForPhase} from './videoProgress'
|
||||||
|
|
||||||
|
describe('videoProgressForPhase', () => {
|
||||||
|
it('maps each phase onto one continuous timeline', () => {
|
||||||
|
expect(videoProgressForPhase('compressing', 0)).toBe(0)
|
||||||
|
expect(videoProgressForPhase('compressing', 1)).toBe(0.4)
|
||||||
|
expect(videoProgressForPhase('uploading', 0)).toBe(0.4)
|
||||||
|
expect(videoProgressForPhase('uploading', 1)).toBe(0.55)
|
||||||
|
expect(videoProgressForPhase('processing', 0)).toBe(0.55)
|
||||||
|
expect(videoProgressForPhase('processing', 1)).toBe(0.95)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clamps invalid phase progress', () => {
|
||||||
|
expect(videoProgressForPhase('uploading', -1)).toBe(0.4)
|
||||||
|
expect(videoProgressForPhase('uploading', 2)).toBe(0.55)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('advanceVideoProgress', () => {
|
||||||
|
it('does not move backwards when a transport retries or falls back', () => {
|
||||||
|
const progressed = advanceVideoProgress(0.5, 'uploading', 0.8)
|
||||||
|
expect(advanceVideoProgress(progressed, 'uploading', 0)).toBe(progressed)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
export type VideoProgressPhase = 'compressing' | 'uploading' | 'processing'
|
||||||
|
|
||||||
|
// Keep progress monotonic across the full client pipeline instead of showing
|
||||||
|
// three separate 0 -> 100 cycles. Processing stops short of complete because
|
||||||
|
// a server-reported 100% can precede the completed blob becoming available.
|
||||||
|
const PHASE_RANGES: Record<VideoProgressPhase, [number, number]> = {
|
||||||
|
compressing: [0, 0.4],
|
||||||
|
uploading: [0.4, 0.55],
|
||||||
|
processing: [0.55, 0.95],
|
||||||
|
}
|
||||||
|
|
||||||
|
export function videoProgressForPhase(
|
||||||
|
phase: VideoProgressPhase,
|
||||||
|
phaseProgress: number,
|
||||||
|
): number {
|
||||||
|
const [start, end] = PHASE_RANGES[phase]
|
||||||
|
const clamped = Math.min(1, Math.max(0, phaseProgress))
|
||||||
|
return start + (end - start) * clamped
|
||||||
|
}
|
||||||
|
|
||||||
|
export function advanceVideoProgress(
|
||||||
|
currentProgress: number,
|
||||||
|
phase: VideoProgressPhase,
|
||||||
|
phaseProgress: number,
|
||||||
|
): number {
|
||||||
|
return Math.max(currentProgress, videoProgressForPhase(phase, phaseProgress))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user