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.