Scope this PR to module-add + probe-only telemetry

Reverts the encoder swap. react-native-compressor stays the native encode
path; expo-bluesky-video-compress is added as a module but only its probe()
function is used, fired from processVideo before compressStarted to emit
the video:upload:probed telemetry event.

This lets the new module ship behind the wall so we can validate probe
reliability and gather bitrate/HDR/codec distributions across the device
matrix without coupling the rollout to a change in the active encoder. A
follow-up PR flips the compress path to the new module once probe data
backs the smart-skip thresholds.

- compress.ts reverts to main's rn-compressor path (matches origin/main)
- COMPRESSION_PASSTHROUGH_BITRATE constant removed (was new-module only)
- 'below-thresholds' skipReason removed (only reachable via new compress path)
- engine label stays native:react-native-compressor@1.13.0
- VideoTelemetry.probed and video:upload:probed event kept; probe is now
  called from processVideo on native, not from compressVideo
This commit is contained in:
vineyardbovines
2026-06-25 19:05:07 -04:00
parent f4b8651708
commit 3da9b0ef75
5 changed files with 59 additions and 108 deletions
+42 -100
View File
@@ -1,32 +1,25 @@
import {getVideoMetaData, Video} from 'react-native-compressor'
import {type ImagePickerAsset} from 'expo-image-picker'
import {
SUPPORTED_MIME_TYPES,
type SupportedMimeTypes,
VIDEO_MAX_SIZE,
} from '#/lib/constants'
import {logger} from '#/logger'
import {
compress,
probe,
type VideoMetadata,
} from '../../../../modules/expo-bluesky-video-compress'
import {
COMPRESSION_MAX_DIMENSION,
COMPRESSION_PASSTHROUGH_BITRATE,
COMPRESSION_TARGET_BITRATE,
} from './constants'
import {SUPPORTED_MIME_TYPES, type SupportedMimeTypes} from '#/lib/constants'
import {type CompressedVideo} from './types'
import {extToMime} from './util'
const MIN_SIZE_FOR_COMPRESSION_BYTES = 25 * 1024 * 1024 // 25mb
export async function compressVideo(
file: ImagePickerAsset,
opts?: {
signal?: AbortSignal
onProgress?: (progress: number) => void
onProbe?: (metadata: VideoMetadata) => void
},
): Promise<CompressedVideo> {
const {onProgress, signal} = opts || {}
if (file.mimeType === 'image/gif') {
// let's hope they're small enough that they don't need compression!
// this compression library doesn't support gifs
// worst case - server rejects them. I think that's fine -sfn
return {
uri: file.uri,
size: file.fileSize ?? -1,
@@ -35,101 +28,50 @@ export async function compressVideo(
}
}
// Pre-check the threshold ourselves so we can label the skip in telemetry.
// rnc would do the same skip internally via minimumFileSizeForCompress, but
// that path is invisible to us.
const isAcceptableFormat = SUPPORTED_MIME_TYPES.includes(
file.mimeType as SupportedMimeTypes,
)
let metadata
try {
metadata = await probe(file.uri)
} catch (e) {
logger.debug('probe failed, falling through to passthrough', {
safeMessage: e,
})
if (
isAcceptableFormat &&
file.fileSize != null &&
file.fileSize < MIN_SIZE_FOR_COMPRESSION_BYTES
) {
return {
uri: file.uri,
size: file.fileSize ?? -1,
size: file.fileSize,
mimeType: file.mimeType ?? 'video/mp4',
passthroughReason: 'compress-error-fallback',
passthroughReason: 'below-byte-threshold',
}
}
opts?.onProbe?.(metadata)
if (!shouldCompress(metadata, isAcceptableFormat)) {
return {
uri: file.uri,
size: metadata.fileSize,
mimeType: file.mimeType ?? 'video/mp4',
passthroughReason: 'below-thresholds',
}
}
const result = await compress(
const compressed = await Video.compress(
file.uri,
{
targetBitrate: COMPRESSION_TARGET_BITRATE,
maxSize: COMPRESSION_MAX_DIMENSION,
codec: 'h264',
},
{
onProgress: opts?.onProgress,
signal: opts?.signal,
compressionMethod: 'manual',
bitrate: 3_000_000, // 3mbps
maxSize: 1920,
// Force a transcode for unacceptable-format files regardless of size.
// rnc's default minimumFileSizeForCompress 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)
})
}
},
},
onProgress,
)
return {
uri: result.uri,
size: result.size,
mimeType: result.mimeType,
}
}
function shouldCompress(
metadata: {
bitrate: number
width: number
height: number
fileSize: number
isHDR: boolean
},
isAcceptableFormat: boolean,
): boolean {
const maxDimension = Math.max(metadata.width, metadata.height)
const bitrateKbps = Math.round(metadata.bitrate / 1000)
const sizeMB = (metadata.fileSize / 1_000_000).toFixed(1)
if (!isAcceptableFormat) {
logger.debug('shouldCompress: yes (unsupported format)')
return true
}
// HDR sources need the SDR BT.709 tone-map in the compress path; otherwise we
// would upload HLG/PQ/Dolby Vision untouched.
if (metadata.isHDR) {
logger.debug('shouldCompress: yes (HDR source)')
return true
}
if (metadata.fileSize > VIDEO_MAX_SIZE) {
logger.debug(`shouldCompress: yes (file too large: ${sizeMB}MB)`)
return true
}
if (
metadata.bitrate <= COMPRESSION_PASSTHROUGH_BITRATE &&
maxDimension <= COMPRESSION_MAX_DIMENSION
) {
logger.debug(
`shouldCompress: no (${bitrateKbps}kbps, ${maxDimension}px, ${sizeMB}MB)`,
)
return false
}
if (metadata.bitrate > COMPRESSION_PASSTHROUGH_BITRATE) {
logger.debug(`shouldCompress: yes (bitrate ${bitrateKbps}kbps)`)
} else {
logger.debug(`shouldCompress: yes (dimension ${maxDimension}px)`)
}
return true
const info = await getVideoMetaData(compressed)
return {uri: compressed, size: info.size, mimeType: extToMime(info.extension)}
}
+1 -5
View File
@@ -5,10 +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
// Native only: source files at or under this bitrate skip compression (paired
// with COMPRESSION_MAX_DIMENSION). Slightly above the encode target so we
// don't re-encode files that are already close to what we'd produce.
export const COMPRESSION_PASSTHROUGH_BITRATE = 5_000_000 // 5 Mbps
// Web only: files under this size skip compression entirely. Native applies
// its own threshold logic inside expo-bluesky-video-compress's probe step.
// its own threshold logic inside react-native-compressor.
export const COMPRESSION_MIN_SIZE_BYTES = 25_000_000
+1 -1
View File
@@ -15,7 +15,7 @@ type MetricFn = <E extends keyof Metrics>(event: E, payload: Metrics[E]) => void
const COMPRESS_ENGINE =
Platform.OS === 'web'
? 'web:mediabunny@1.25.3'
: 'native:expo-bluesky-video-compress@1'
: 'native:react-native-compressor@1.13.0'
type Phase = 'compress' | 'upload' | 'processing'
-1
View File
@@ -5,7 +5,6 @@
export type VideoCompressSkipReason =
| 'gif'
| 'below-byte-threshold'
| 'below-thresholds'
| 'no-webcodecs'
| 'compress-error-fallback'
+15 -1
View File
@@ -1,3 +1,4 @@
import {Platform} from 'react-native'
import {type ImagePickerAsset} from 'expo-image-picker'
import {type AppBskyVideoDefs, type AtpAgent, type BlobRef} from '@atproto/api'
import {type I18n} from '@lingui/core'
@@ -17,6 +18,7 @@ import {uploadVideo} from '#/lib/media/video/upload'
import {createVideoAgent} from '#/lib/media/video/util'
import {isNetworkError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {probe} from '../../../../../modules/expo-bluesky-video-compress'
type CaptionsTrack = {lang: string; file: File}
@@ -280,6 +282,19 @@ export async function processVideo(
i18n: I18n,
telemetry: VideoTelemetry,
) {
// Native-only side channel: fire telemetry.probed with raw container
// metadata (bitrate, codec, isHDR, frame rate, etc.) that rn-compressor
// does not surface. The compress path itself is unchanged - probe data is
// purely for analytics to validate future smart-skip thresholds.
if (Platform.OS !== 'web') {
try {
const metadata = await probe(asset.uri)
telemetry.probed(metadata)
} catch (e) {
logger.debug('video probe failed', {safeMessage: e})
}
}
let video: CompressedVideo | undefined
try {
telemetry.compressStarted()
@@ -288,7 +303,6 @@ export async function processVideo(
dispatch({type: 'update_progress', progress: trunc2dp(num), signal})
},
signal,
onProbe: metadata => telemetry.probed(metadata),
})
} catch (e) {
const message = getCompressErrorMessage(e, i18n)