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:
+42
-100
@@ -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)}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
export type VideoCompressSkipReason =
|
||||
| 'gif'
|
||||
| 'below-byte-threshold'
|
||||
| 'below-thresholds'
|
||||
| 'no-webcodecs'
|
||||
| 'compress-error-fallback'
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user