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 {type ImagePickerAsset} from 'expo-image-picker'
|
||||||
|
|
||||||
import {
|
import {SUPPORTED_MIME_TYPES, type SupportedMimeTypes} from '#/lib/constants'
|
||||||
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 {type CompressedVideo} from './types'
|
import {type CompressedVideo} from './types'
|
||||||
|
import {extToMime} from './util'
|
||||||
|
|
||||||
|
const MIN_SIZE_FOR_COMPRESSION_BYTES = 25 * 1024 * 1024 // 25mb
|
||||||
|
|
||||||
export async function compressVideo(
|
export async function compressVideo(
|
||||||
file: ImagePickerAsset,
|
file: ImagePickerAsset,
|
||||||
opts?: {
|
opts?: {
|
||||||
signal?: AbortSignal
|
signal?: AbortSignal
|
||||||
onProgress?: (progress: number) => void
|
onProgress?: (progress: number) => void
|
||||||
onProbe?: (metadata: VideoMetadata) => void
|
|
||||||
},
|
},
|
||||||
): Promise<CompressedVideo> {
|
): Promise<CompressedVideo> {
|
||||||
|
const {onProgress, signal} = opts || {}
|
||||||
|
|
||||||
if (file.mimeType === 'image/gif') {
|
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 {
|
return {
|
||||||
uri: file.uri,
|
uri: file.uri,
|
||||||
size: file.fileSize ?? -1,
|
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(
|
const isAcceptableFormat = SUPPORTED_MIME_TYPES.includes(
|
||||||
file.mimeType as SupportedMimeTypes,
|
file.mimeType as SupportedMimeTypes,
|
||||||
)
|
)
|
||||||
|
if (
|
||||||
let metadata
|
isAcceptableFormat &&
|
||||||
try {
|
file.fileSize != null &&
|
||||||
metadata = await probe(file.uri)
|
file.fileSize < MIN_SIZE_FOR_COMPRESSION_BYTES
|
||||||
} catch (e) {
|
) {
|
||||||
logger.debug('probe failed, falling through to passthrough', {
|
|
||||||
safeMessage: e,
|
|
||||||
})
|
|
||||||
return {
|
return {
|
||||||
uri: file.uri,
|
uri: file.uri,
|
||||||
size: file.fileSize ?? -1,
|
size: file.fileSize,
|
||||||
mimeType: file.mimeType ?? 'video/mp4',
|
mimeType: file.mimeType ?? 'video/mp4',
|
||||||
passthroughReason: 'compress-error-fallback',
|
passthroughReason: 'below-byte-threshold',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
opts?.onProbe?.(metadata)
|
const compressed = await Video.compress(
|
||||||
|
|
||||||
if (!shouldCompress(metadata, isAcceptableFormat)) {
|
|
||||||
return {
|
|
||||||
uri: file.uri,
|
|
||||||
size: metadata.fileSize,
|
|
||||||
mimeType: file.mimeType ?? 'video/mp4',
|
|
||||||
passthroughReason: 'below-thresholds',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await compress(
|
|
||||||
file.uri,
|
file.uri,
|
||||||
{
|
{
|
||||||
targetBitrate: COMPRESSION_TARGET_BITRATE,
|
compressionMethod: 'manual',
|
||||||
maxSize: COMPRESSION_MAX_DIMENSION,
|
bitrate: 3_000_000, // 3mbps
|
||||||
codec: 'h264',
|
maxSize: 1920,
|
||||||
},
|
// Force a transcode for unacceptable-format files regardless of size.
|
||||||
{
|
// rnc's default minimumFileSizeForCompress would otherwise pass small
|
||||||
onProgress: opts?.onProgress,
|
// unacceptable-format files through unchanged and the server would
|
||||||
signal: opts?.signal,
|
// 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 {
|
const info = await getVideoMetaData(compressed)
|
||||||
uri: result.uri,
|
|
||||||
size: result.size,
|
return {uri: compressed, size: info.size, mimeType: extToMime(info.extension)}
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,6 @@
|
|||||||
export const COMPRESSION_TARGET_BITRATE = 3_000_000 // 3 Mbps
|
export const COMPRESSION_TARGET_BITRATE = 3_000_000 // 3 Mbps
|
||||||
// Output dimension cap when compressing, and skip threshold for source files.
|
// Output dimension cap when compressing, and skip threshold for source files.
|
||||||
export const COMPRESSION_MAX_DIMENSION = 1920
|
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
|
// 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
|
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 =
|
const COMPRESS_ENGINE =
|
||||||
Platform.OS === 'web'
|
Platform.OS === 'web'
|
||||||
? 'web:mediabunny@1.25.3'
|
? 'web:mediabunny@1.25.3'
|
||||||
: 'native:expo-bluesky-video-compress@1'
|
: 'native:react-native-compressor@1.13.0'
|
||||||
|
|
||||||
type Phase = 'compress' | 'upload' | 'processing'
|
type Phase = 'compress' | 'upload' | 'processing'
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
export type VideoCompressSkipReason =
|
export type VideoCompressSkipReason =
|
||||||
| 'gif'
|
| 'gif'
|
||||||
| 'below-byte-threshold'
|
| 'below-byte-threshold'
|
||||||
| 'below-thresholds'
|
|
||||||
| 'no-webcodecs'
|
| 'no-webcodecs'
|
||||||
| 'compress-error-fallback'
|
| 'compress-error-fallback'
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import {Platform} from 'react-native'
|
||||||
import {type ImagePickerAsset} from 'expo-image-picker'
|
import {type ImagePickerAsset} from 'expo-image-picker'
|
||||||
import {type AppBskyVideoDefs, type AtpAgent, type BlobRef} from '@atproto/api'
|
import {type AppBskyVideoDefs, type AtpAgent, type BlobRef} from '@atproto/api'
|
||||||
import {type I18n} from '@lingui/core'
|
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 {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 {probe} from '../../../../../modules/expo-bluesky-video-compress'
|
||||||
|
|
||||||
type CaptionsTrack = {lang: string; file: File}
|
type CaptionsTrack = {lang: string; file: File}
|
||||||
|
|
||||||
@@ -280,6 +282,19 @@ export async function processVideo(
|
|||||||
i18n: I18n,
|
i18n: I18n,
|
||||||
telemetry: VideoTelemetry,
|
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
|
let video: CompressedVideo | undefined
|
||||||
try {
|
try {
|
||||||
telemetry.compressStarted()
|
telemetry.compressStarted()
|
||||||
@@ -288,7 +303,6 @@ export async function processVideo(
|
|||||||
dispatch({type: 'update_progress', progress: trunc2dp(num), signal})
|
dispatch({type: 'update_progress', progress: trunc2dp(num), signal})
|
||||||
},
|
},
|
||||||
signal,
|
signal,
|
||||||
onProbe: metadata => telemetry.probed(metadata),
|
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const message = getCompressErrorMessage(e, i18n)
|
const message = getCompressErrorMessage(e, i18n)
|
||||||
|
|||||||
Reference in New Issue
Block a user