From 5cf6d8fa21f6572bebae61a8e6e981543035db24 Mon Sep 17 00:00:00 2001 From: Spence Pope Date: Thu, 6 Aug 2026 08:16:18 -0400 Subject: [PATCH] Use toolbox video compressor (#11246) --- package.json | 1 + pnpm-lock.yaml | 17 ++++++++++++ src/analytics/metrics/types.ts | 4 +-- src/lib/media/video/compress.ts | 44 +++++++++++++++----------------- src/lib/media/video/constants.ts | 4 +-- src/lib/media/video/telemetry.ts | 2 +- src/lib/media/video/types.ts | 2 +- 7 files changed, 45 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index 2759a59119..6af3698d37 100644 --- a/package.json +++ b/package.json @@ -112,6 +112,7 @@ "@bsky.app/sift": "^0.3.9", "@bsky.app/tapper": "^0.6.1", "@bsky.app/video": "0.3.6", + "@bsky.app/video-compressor": "0.2.0", "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", "@expo/html-elements": "^0.12.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1ec1f5267c..ce0b6b65f5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -289,6 +289,9 @@ importers: '@bsky.app/video': specifier: 0.3.6 version: 0.3.6(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + '@bsky.app/video-compressor': + specifier: 0.2.0 + version: 0.2.0(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) '@emoji-mart/data': specifier: ^1.2.1 version: 1.2.1 @@ -1698,6 +1701,13 @@ packages: react-native: '*' tlds: '*' + '@bsky.app/video-compressor@0.2.0': + resolution: {integrity: sha512-hQPT/zxy3MoODuN7QM5Q0jA5anrrwKfka4o9BTz821sAaiRWW8E4vdw4xsbuhfUXRNOSXknQbDHch2Toh178Xw==} + peerDependencies: + expo: '*' + react: '*' + react-native: '*' + '@bsky.app/video@0.3.6': resolution: {integrity: sha512-Ny7vz9v1lQOw5Llw4xejtJo/Dp/lmdRw1XdUJucOXlq6puXO93tv3mNXUIIyOEVWEwDPg8j/kSQxG/mGy+/hdQ==} peerDependencies: @@ -10439,6 +10449,13 @@ snapshots: react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) tlds: 1.261.0 + '@bsky.app/video-compressor@0.2.0(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': + dependencies: + expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + mediabunny: 1.49.0 + react: 19.1.0 + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + '@bsky.app/video@0.3.6(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': dependencies: expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) diff --git a/src/analytics/metrics/types.ts b/src/analytics/metrics/types.ts index 592f9a563d..b5e01d11e3 100644 --- a/src/analytics/metrics/types.ts +++ b/src/analytics/metrics/types.ts @@ -1388,7 +1388,7 @@ export type Events = { // === Video upload funnel (Frontend Spec section D) === // Every event carries uploadId (client-generated UUID, ties one upload // session end-to-end) + engine (compression engine id, e.g. - // native:react-native-compressor@1.13.0). jobId is added once the server + // native:@bsky.app/video-compressor@0.2.0). jobId is added once the server // returns it. Sizes / codecs / dimensions / timings only - never content. 'video:upload:picked': { uploadId: string @@ -1407,7 +1407,7 @@ export type Events = { // Native-only. Raw container metadata returned by the new module's probe() // (bitrate, codec, HDR, frame rate, rotation, etc.). Fires once per upload // between compressStarted and the compressSkipped/compressCompleted decision. - // The web (mediabunny) and legacy rn-compressor engines do not surface this. + // The web mediabunny engine also emits this from its own probe. 'video:upload:probed': { uploadId: string engine: string diff --git a/src/lib/media/video/compress.ts b/src/lib/media/video/compress.ts index 72fb2a246c..d38c1cdecf 100644 --- a/src/lib/media/video/compress.ts +++ b/src/lib/media/video/compress.ts @@ -1,11 +1,13 @@ -import {getVideoMetaData, Video} from 'react-native-compressor' import {type ImagePickerAsset} from 'expo-image-picker' +import {compress, probe, type VideoMetadata} from '@bsky.app/video-compressor' import {SUPPORTED_MIME_TYPES, type SupportedMimeTypes} from '#/lib/constants' import {logger} from '#/logger' -import {probe} from '../../../../modules/expo-bluesky-video-compress' +import { + COMPRESSION_MAX_DIMENSION, + COMPRESSION_TARGET_BITRATE, +} from './constants' import {type CompressedVideo, type ProbedMetadata} from './types' -import {extToMime} from './util' const MIN_SIZE_FOR_COMPRESSION_BYTES = 25 * 1024 * 1024 // 25mb @@ -23,7 +25,7 @@ export async function compressVideo( // future smart-skip thresholds. Failures must not block the upload. if (onProbe && file.mimeType !== 'image/gif') { try { - onProbe(await probe(file.uri)) + onProbe(toProbedMetadata(await probe(file.uri))) } catch (e) { logger.debug('video probe failed', {safeMessage: e}) } @@ -60,31 +62,27 @@ export async function compressVideo( } } - const compressed = await Video.compress( + return compress( file.uri, { - compressionMethod: 'manual', - bitrate: 3_000_000, // 3mbps - maxSize: 1920, + targetBitrate: COMPRESSION_TARGET_BITRATE, + maxSize: COMPRESSION_MAX_DIMENSION, + codec: 'auto', + frameRateCap: 30, + mimeType: file.mimeType, + fileSize: file.fileSize, // Force a transcode for unacceptable-format files regardless of size. - // rnc's default minimumFileSizeForCompress would otherwise pass small + // The compressor's default threshold would otherwise pass small // unacceptable-format files through unchanged and the server would // reject them. Acceptable formats are already short-circuited above so // they never reach this call. - // WARNING: this ONE SPECIFIC ARG is in MB -sfn - minimumFileSizeForCompress: 0, - getCancellationId: id => { - if (signal) { - signal.addEventListener('abort', () => { - Video.cancelCompression(id) - }) - } - }, + passthroughBelowBytes: 0, + passthroughGif: false, }, - onProgress, + {onProgress, signal}, ) - - const info = await getVideoMetaData(compressed) - - return {uri: compressed, size: info.size, mimeType: extToMime(info.extension)} +} + +function toProbedMetadata(metadata: VideoMetadata): ProbedMetadata { + return metadata } diff --git a/src/lib/media/video/constants.ts b/src/lib/media/video/constants.ts index 60580efc8c..0f74016147 100644 --- a/src/lib/media/video/constants.ts +++ b/src/lib/media/video/constants.ts @@ -5,6 +5,6 @@ export const COMPRESSION_TARGET_BITRATE = 3_000_000 // 3 Mbps // Output dimension cap when compressing, and skip threshold for source files. export const COMPRESSION_MAX_DIMENSION = 1920 -// Web only: files under this size skip compression entirely. Native applies -// its own threshold logic inside react-native-compressor. +// Web only: files under this size skip compression entirely. Native uses a +// 25 MiB threshold in compress.ts for compatibility with the previous engine. export const COMPRESSION_MIN_SIZE_BYTES = 25_000_000 diff --git a/src/lib/media/video/telemetry.ts b/src/lib/media/video/telemetry.ts index 9181ec002b..188abb6d80 100644 --- a/src/lib/media/video/telemetry.ts +++ b/src/lib/media/video/telemetry.ts @@ -18,7 +18,7 @@ type MetricFn = (event: E, payload: Metrics[E]) => void const COMPRESS_ENGINE = Platform.OS === 'web' ? 'web:mediabunny@1.25.3' - : 'native:react-native-compressor@1.13.0' + : 'native:@bsky.app/video-compressor@0.2.0' type Phase = 'compress' | 'upload' | 'processing' diff --git a/src/lib/media/video/types.ts b/src/lib/media/video/types.ts index 1d2062ce00..6431c567fa 100644 --- a/src/lib/media/video/types.ts +++ b/src/lib/media/video/types.ts @@ -22,7 +22,7 @@ export type CompressedVideo = { } // Source container metadata read off the input before any encoding decision. -// Same shape across native (expo-bluesky-video-compress probe) and web +// Same shape across native (@bsky.app/video-compressor probe) and web // (mediabunny Input + track inspection). Numbers are raw - no bucketing. export type ProbedMetadata = { mimeType: string