From 1790fddc78c2dc28940932922c0c5ad0b4918d49 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 26 May 2026 17:23:43 +0300 Subject: [PATCH] Enable 300mb video uploads (#10497) --- src/analytics/features/types.ts | 1 + src/lib/constants.ts | 3 ++- src/lib/media/video/compress.ts | 1 + src/lib/media/video/compress.web.ts | 12 +++++++++--- src/lib/media/video/errors.ts | 2 +- src/view/com/composer/Composer.tsx | 18 +++++++++++------ src/view/com/composer/state/video.ts | 29 ++++++++++++++++++++++------ 7 files changed, 49 insertions(+), 17 deletions(-) diff --git a/src/analytics/features/types.ts b/src/analytics/features/types.ts index 39df48e532..bd48bd5851 100644 --- a/src/analytics/features/types.ts +++ b/src/analytics/features/types.ts @@ -16,6 +16,7 @@ export enum Features { DmsNewMessageComposerEnable = 'dms:new_message_composer:enable', ComposerLanguageDetectionEnable = 'composer:language_detection:enable', PostGalleryEmbedEnable = 'post_gallery_embed:enable', + LargeVideoUploads = 'large_video_uploads:enable', AATest = 'aa-test', } diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 80e697ace2..59d82e94f9 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -188,7 +188,8 @@ export const VIDEO_MAX_DURATION_MS = 3 * 60 * 1000 // 3 minutes in milliseconds * Maximum size of a video in megabytes, _not_ mebibytes. Backend uses * ISO megabytes. */ -export const VIDEO_MAX_SIZE = 1000 * 1000 * 100 // 100mb +export const VIDEO_MAX_SIZE_REDUCED = 1000 * 1000 * 100 // 100mb +export const VIDEO_MAX_SIZE = 3000 * 1000 * 100 // 300mb export const SUPPORTED_MIME_TYPES = [ 'video/mp4', diff --git a/src/lib/media/video/compress.ts b/src/lib/media/video/compress.ts index 1d00bfcea1..60eda2dcee 100644 --- a/src/lib/media/video/compress.ts +++ b/src/lib/media/video/compress.ts @@ -12,6 +12,7 @@ export async function compressVideo( opts?: { signal?: AbortSignal onProgress?: (progress: number) => void + TEMP_enableLargeVideoUploads?: boolean }, ): Promise { const {onProgress, signal} = opts || {} diff --git a/src/lib/media/video/compress.web.ts b/src/lib/media/video/compress.web.ts index 83fdfde533..63921fe8e7 100644 --- a/src/lib/media/video/compress.web.ts +++ b/src/lib/media/video/compress.web.ts @@ -1,22 +1,28 @@ import {type ImagePickerAsset} from 'expo-image-picker' -import {VIDEO_MAX_SIZE} from '#/lib/constants' +import {VIDEO_MAX_SIZE, VIDEO_MAX_SIZE_REDUCED} from '#/lib/constants' import {VideoTooLargeError} from '#/lib/media/video/errors' import {type CompressedVideo} from './types' // doesn't actually compress, converts to ArrayBuffer export async function compressVideo( asset: ImagePickerAsset, - _opts?: { + opts?: { signal?: AbortSignal onProgress?: (progress: number) => void + TEMP_enableLargeVideoUploads?: number }, ): Promise { const {mimeType, base64} = parseDataUrl(asset.uri) const blob = base64ToBlob(base64, mimeType) const uri = URL.createObjectURL(blob) - if (blob.size > VIDEO_MAX_SIZE) { + if ( + blob.size > + (opts?.TEMP_enableLargeVideoUploads + ? VIDEO_MAX_SIZE + : VIDEO_MAX_SIZE_REDUCED) + ) { throw new VideoTooLargeError() } diff --git a/src/lib/media/video/errors.ts b/src/lib/media/video/errors.ts index 945f89cdda..cbf3083d1f 100644 --- a/src/lib/media/video/errors.ts +++ b/src/lib/media/video/errors.ts @@ -1,6 +1,6 @@ export class VideoTooLargeError extends Error { constructor() { - super('Videos cannot be larger than 100 MB') + super('Videos cannot be larger than 300 MB') this.name = 'VideoTooLargeError' } } diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 2201a6a2a4..033341f2c2 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -222,6 +222,10 @@ export const ComposePost = ({ const [publishingStage, setPublishingStage] = useState('') const [error, setError] = useState('') + const enableLargeVideoUploads = ax.features.enabled( + ax.features.LargeVideoUploads, + ) + /** * Track when a draft was created so we can measure draft age in metrics. * Set when a draft is loaded via handleSelectDraft. @@ -344,9 +348,10 @@ export const ComposePost = ({ currentDid, abortController.signal, i18n, + enableLargeVideoUploads, ) }, - [i18n, agent, currentDid, composerDispatch], + [i18n, agent, currentDid, composerDispatch, enableLargeVideoUploads], ) const onInitVideo = useNonReactiveCallback(() => { @@ -491,6 +496,7 @@ export const ComposePost = ({ currentDid, abortController.signal, i18n, + enableLargeVideoUploads, ) } catch (e) { logger.error('Failed to restore video from draft', { @@ -499,7 +505,7 @@ export const ComposePost = ({ }) } }, - [i18n, agent, currentDid, composerDispatch], + [i18n, agent, currentDid, composerDispatch, enableLargeVideoUploads], ) const handleSelectDraft = useCallback( @@ -810,7 +816,7 @@ export const ComposePost = ({ )), ) - const getFilteredThread = (): { + const getFilteredThread = useCallback((): { type: 'none' | 'trailing-only' | 'non-trailing' filteredThread: ThreadDraft } => { @@ -838,7 +844,7 @@ export const ComposePost = ({ type: hasNonTrailingEmpty ? 'non-trailing' : 'trailing-only', filteredThread, } - } + }, [thread]) const onPressPublish = useCallback(async () => { if (isPublishing) { @@ -1011,7 +1017,7 @@ export const ComposePost = ({ setLangPrefs.savePostLanguageToHistory() if (initQuote) { // We want to wait for the quote count to update before we call `onPost`, which will refetch data - whenAppViewReady(agent, initQuote.uri, res => { + void whenAppViewReady(agent, initQuote.uri, res => { const anchor = res.data.thread.at(0) if ( AppBskyUnspeccedDefs.isThreadItemPost(anchor?.value) && @@ -1059,7 +1065,6 @@ export const ComposePost = ({ l, ax, agent, - thread, canPost, isPublishing, currentLanguages, @@ -1077,6 +1082,7 @@ export const ComposePost = ({ cleanupPublishedDraft, loadedDraftCreatedAt, emptyPostsPromptControl, + getFilteredThread, ]) const handleConfirmSkipEmpty = () => { diff --git a/src/view/com/composer/state/video.ts b/src/view/com/composer/state/video.ts index 9db18610a4..b99d0a1e9d 100644 --- a/src/view/com/composer/state/video.ts +++ b/src/view/com/composer/state/video.ts @@ -264,6 +264,7 @@ export async function processVideo( did: string, signal: AbortSignal, i18n: I18n, + TEMP_enableLargeVideoUploads: boolean, ) { let video: CompressedVideo | undefined try { @@ -272,9 +273,14 @@ export async function processVideo( dispatch({type: 'update_progress', progress: trunc2dp(num), signal}) }, signal, + TEMP_enableLargeVideoUploads, }) } catch (e) { - const message = getCompressErrorMessage(e, i18n) + const message = getCompressErrorMessage( + e, + i18n, + TEMP_enableLargeVideoUploads, + ) if (message !== null) { dispatch({ type: 'to_error', @@ -303,7 +309,7 @@ export async function processVideo( }, }) } catch (e) { - const message = getUploadErrorMessage(e, i18n) + const message = getUploadErrorMessage(e, i18n, TEMP_enableLargeVideoUploads) if (message !== null) { dispatch({ type: 'to_error', @@ -387,20 +393,30 @@ export async function processVideo( } } -function getCompressErrorMessage(e: unknown, i18n: I18n): string | null { +function getCompressErrorMessage( + e: unknown, + i18n: I18n, + TEMP_enableLargeVideoUploads: boolean, +): string | null { + const videoSize = TEMP_enableLargeVideoUploads ? 300 : 100 if (e instanceof AbortError) { return null } if (e instanceof VideoTooLargeError) { return i18n._( - msg`The selected video is larger than 100 MB. Please try again with a smaller file.`, + msg`The selected video is larger than ${videoSize} MB. Please try again with a smaller file.`, ) } logger.error('Error compressing video', {safeMessage: e}) return i18n._(msg`An error occurred while compressing the video.`) } -function getUploadErrorMessage(e: unknown, i18n: I18n): string | null { +function getUploadErrorMessage( + e: unknown, + i18n: I18n, + TEMP_enableLargeVideoUploads: boolean, +): string | null { + const videoSize = TEMP_enableLargeVideoUploads ? 300 : 100 if (e instanceof AbortError) { return null } @@ -430,8 +446,9 @@ function getUploadErrorMessage(e: unknown, i18n: I18n): string | null { msg`Your account is not yet old enough to upload videos. Please try again later.`, ) case 'file size (100000001 bytes) is larger than the maximum allowed size (100000000 bytes)': + case 'file size (300000001 bytes) is larger than the maximum allowed size (300000000 bytes)': return i18n._( - msg`The selected video is larger than 100 MB. Please try again with a smaller file.`, + msg`The selected video is larger than ${videoSize} MB. Please try again with a smaller file.`, ) case 'Confirm your email address to upload videos': return i18n._(msg`Please confirm your email address to upload videos.`)