Defines ProbedMetadata as the shared shape and emits it from both compress
entry points via a new onProbe opt:
- compress.ts (native) calls expo-bluesky-video-compress's probe() before
rn-compressor takes over. Pure side effect for telemetry; no decision is
gated on the result. Same call moved out of processVideo so the seam is
uniform across platforms.
- compress.web.ts probes via mediabunny: opens the Input, reads codec /
coded dims / rotation / HDR off the primary video track, samples
computePacketStats(100) for bitrate and frame rate, and reports
hasAudio off the primary audio track. Skips on gif and missing
WebCodecs - both already cause the compression path to fall through.
processVideo now passes onProbe: telemetry.probed straight into
compressVideo. telemetry.probed types its arg as ProbedMetadata (was
the new module's VideoMetadata) so it stays platform-neutral.
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
Adds video:upload:probed event (uploadId/engine + raw container metadata:
bitrate, codec, isHDR, frame rate, rotation, etc.) wired through a new
VideoTelemetry.probed method. compressVideo gains an onProbe opt and fires
it once after the probe call succeeds, before shouldCompress is evaluated.
Renames the native engine label from native:react-native-compressor@1.13.0
to native:expo-bluesky-video-compress@1 so probed events stay paired with
the engine that can actually surface this data; web/mediabunny is unchanged
and will simply never call telemetry.probed().
Drops the parallel composer:video:probe* events added in 127d0288 in favor
of main's video:upload:* funnel from #10991. Reshapes the new module's
compressVideo to set passthroughReason on every skip branch so it conforms
to the VideoCompressSkipReason contract, including a new 'below-thresholds'
reason for the shouldCompress passthrough. Probe data wiring into the
funnel comes in a follow-up commit.
* origin/main:
Focus composer input when editable (#10982)
Fix issue with stale unread message counts (#10953)
[APP-2429] Add Sunlight and Twilight color options to the Invite Friends QR code (#10921)
signup: align "Contact support" to the right on larger displays (#10978)
Set a min height on native Menus (#10956)
Tweak strings and context (#10928)
Fix internal repo sync broken by actions/checkout v6 bump (#10933)
Fix setState-in-render warning from convo cache subscription (#10934)
Add light haptics to Edit Profile Button for labellers (#10948)
Remove old message composer (#10951)
Fix embeds overlapping each other in chat (#10949)
Add mark all as read to chat settings (#10973)
Add TestFlight group selection to iOS build workflow (#10979)
Nightly source-language update
bskyweb: add isPartOf jsonld post attr (#10945)
Per Frontend Spec (A2.4 pre-merge note): MAX_UPLOAD_SIZE was hardcoded to
100 MB and would force-compress 100-300 MB files that should skip. Reads
from VIDEO_MAX_SIZE now. Pulled the target/passthrough bitrate and max
dimension out of compress.ts into src/lib/media/video/constants.ts so the
compression contract lives in one place without bloating the global
constants file.
- Android non-AAC audio is now transcoded to AAC (matching iOS) via a
source-decoder -> AAC-encoder pre-pass that captures the encoder's
output format before the muxer starts, then writes buffered samples
after the video pipeline finishes. Falls back to dropping audio if
the transcode fails.
- compress.ts wraps probe() in try/catch and falls through to
passthrough on failure instead of throwing.
- Probers expose isHDR (HLG/PQ via color transfer, plus Dolby Vision
codecs/mimes); shouldCompress forces compression for HDR sources so
the SDR BT.709 path always runs. Also sets
KEY_COLOR_TRANSFER_REQUEST=SDR on the Android decoder (API 31+) so
HDR sources tone-map to SDR pixels instead of being mislabeled.
N1: Delete unused CodecSelector.selectEncoder. VideoCompressor calls
findEncoder directly; selectEncoder was dead since the initial commit.
N2: Switch Android output filename from System.currentTimeMillis() to
UUID.randomUUID(). Eliminates the collision risk when two compressions
start in the same millisecond.
N3: Guard CheckedContinuation against double-resume in both
processVideoTrack and processAudioTrack. AVAssetWriter shouldn't
re-invoke the requestMediaDataWhenReady block after markAsFinished, but
if it did, the previous code would crash via CheckedContinuation's
double-resume detection. Added a `finished` flag plus a local `finish`
closure that no-ops on second call.
N4: DataRateLimits window duration changed from Int `1` to Double `1.0`.
VideoToolbox expects the duration value as a CFNumber with float
semantics; Int bridges to NSNumber(int) which works on most iOS
versions but is not spec-correct.
B1: Android encoder format now sets KEY_COLOR_STANDARD = BT709,
KEY_COLOR_TRANSFER = SDR_VIDEO, KEY_COLOR_RANGE = LIMITED (API 24+).
Previously the encoder inherited or emitted default color metadata,
which meant HDR sources produced incorrectly-tagged output on Android.
iOS already had AVVideoColorPropertiesKey set correctly.
B2: Replace single currentCompressor reference with a per-job map
keyed by jobId. The cancel function now takes a jobId and only cancels
that specific job. Prevents the previous race where a second compress
call overwrote the reference and made the first job uncancellable.
B3: Clamp frameRateCap to >= 1 at the native module boundary. Previously
a value of 0 from JS would cause divide-by-zero (Android Long division
ArithmeticException, iOS CMTime Infinity / Int32(0) frameDuration trap).
Replaces react-native-compressor's video path with a local Expo module.
Native pipeline:
- iOS: AVAssetReader + AVMutableVideoComposition + AVAssetWriter with
VideoToolbox encode, BGRA reader for HDR -> SDR conversion, BT.709
color tagging, DataRateLimits hard cap, AAC re-encode.
- Android: MediaExtractor -> MediaCodec(decoder) -> GL pipeline ->
MediaCodec(encoder) -> MediaMuxer. BITRATE_MODE_CBR for tight target
enforcement, GL transform-matrix rotation (no double-rotation),
raw AAC passthrough, hardware encoder selection with software
fallback, QTI AVC denylist.
Codec selection: 'auto' resolves to h264 (HLS pipeline + licensing).
HEVC remains opt-in via codec: 'hevc' for future feature-flagged use.
compress.ts adds probe-based smart-skip: clips that are already at or
below 5 Mbps / 1920px / 100MB bypass re-encoding entirely.
react-native-compressor stays as a dep for now since pickVideo and
VideoTranscodeBackdrop still use its non-compression helpers; full
removal is a follow-up.