Expo Video Compression Isn't a Camera Setting. Do It on Upload.

You record a one-minute clip with expo-camera, check the file, and it's 95 MB. Nothing in the API lets you turn that down: no bitrate option, no fps option, no quality ladder. Every system downstream inherits that number, from your S3 bill to your users' cellular data to the 25 MB cap on the transcription endpoint you were planning to send it to.
Quick answer: Expo video compression is needed because expo-camera records at roughly 12.7 Mbps, so a one-minute 720p capture lands near 95 MB, andrecordAsyncexposes no bitrate or fps setting to lower it. On the device,react-native-compressorcan re-encode before upload, at the cost of battery, thermal throttling, and a quality target you can't audit. The more durable fix is to upload the capture once and normalize it server-side: send the file to FFmpeg Micro as one API call and get back a 2 Mbps H.264 MP4 plus any derived variants you need, with no encoder to host on the phone or your own machines.
Why a one-minute expo-camera clip is 95 MB
The size comes from bitrate, not resolution. 95 MB over 60 seconds works out to about 12.7 Mbps, and 720p30 H.264 looks fine at 2 Mbps. expo-camera is handing you a near-master file from the platform recorder, and that's the documented complaint in expo/expo issue #33042, titled "[expo-camera] video files are huge (95mb per minute @ 720p) and don't allow any bitrate/fps customisation."
The cost lands in three places. A UGC app collecting 1,000 sixty-second testimonials a month stores 95 GB of raw captures: about $2.19 at S3 Standard's $0.023 per GB-month, which sounds harmless until the $0.09 per GB egress makes a single full read pass $8.55. Normalize the same clips to 16 MB and that month costs roughly $0.37 to store and $1.44 to serve. Meanwhile the user on a 5 GB plan just spent 2% of it on one upload, and if you were feeding the file to OpenAI's transcription endpoint, it bounced off the 25 MB request cap before you ever got a word back.
What expo-camera actually lets you control
The recording options in expo-camera are about stopping the recording, not shaping it. recordAsync gives you maxDuration in seconds, mute to drop the audio track, and on iOS a codec choice. There is no bitrate parameter and no frame-rate parameter, which is precisely what the open issue is asking for.
Two of those look more useful than they are. mute: true removes an audio track running at maybe 128 kbps out of 12,700, so it saves about 1% of the file. Setting codec: 'hvc1' on iOS switches to HEVC and can genuinely halve the bitrate at the same perceptual quality, but then you're shipping HEVC to Android devices and browsers that won't decode it, and HEVC in MP4 carries its own tagging trap once anything touches the container. Clamping maxDuration is the only lever that reliably bounds file size, and it does so by cutting the video short.
Compressing video before upload in React Native, on the device
On-device compression in React Native works, with a narrower blast radius than people expect. react-native-compressor calls the platform hardware encoder and takes an explicit bitrate:
import { Video } from 'react-native-compressor';
const compressedUri = await Video.compress(
sourceUri,
{ compressionMethod: 'manual', bitrate: 2_000_000, maxSize: 1280 },
(progress) => setProgress(progress)
);
expo-image-and-video-compressor does the same job with an Expo-shaped API. Both are hardware-accelerated, so a one-minute clip re-encodes in seconds rather than minutes.
What neither can do is anything filter-shaped. No burned-in captions, no watermark, no reframe to 9:16, no audio normalization, no thumbnail extraction. That used to be FFmpegKit's territory, and FFmpegKit is retired: Taner Sener's "Saying Goodbye to FFmpegKit" announcement and arthenica/ffmpeg-kit issue #1099 closed the door, and the release binaries now return 404, so ffmpeg-kit-react-native is not an option you can install today. The successor effort is source-only. On a managed Expo workflow you'd also be ejecting to ship a 30 MB native binary you then have to keep current.
There's a second cost that only shows up in production. The hardware encoder shares silicon with the camera you just finished using, so compressing immediately after a long capture on a warm phone means the encode competes with thermal throttling, and on older Android devices that turns a five-second job into thirty.
Upload once, normalize on the server
Server-side normalization inverts the problem: the phone's only job is to move bytes, and every decision about bitrate, format, captions, and crop happens somewhere you can change without shipping an app update.
- Ask your backend for a presigned S3 PUT URL and upload the raw capture directly from the device using a background-capable upload task, so a backgrounded app doesn't kill the transfer.
- When the PUT succeeds, your backend submits one processing job against the object, passing a presigned GET URL rather than downloading the file into your own server first. Handing FFmpeg the presigned URL keeps 95 MB out of your API process entirely.
- The job runs asynchronously and calls your webhook when the output is ready. A one-minute 720p re-encode is a few seconds of work, but treat it as async anyway, because the retry and idempotency patterns for long-running video jobs are the part that keeps a queue honest at 1,000 clips a month.
- Store the normalized output, delete the raw capture on a short lifecycle rule, and fan out any derived variants from the normalized file.
Here's the encode itself, as the raw command:
ffmpeg -i capture.mov \
-c:v libx264 -preset veryfast -profile:v high -level 4.0 \
-b:v 2000k -maxrate 2400k -bufsize 4000k \
-vf "scale=-2:720,fps=30" \
-c:a aac -b:a 128k -ac 2 \
-movflags +faststart \
normalized.mp4
And the same job as one API call, with no encoder to install and no servers to run:
curl -X POST https://api.ffmpeg-micro.com/v1/jobs \
-H "Authorization: Bearer $FFMPEG_MICRO_KEY" \
-H "Content-Type: application/json" \
-d '{
"input_url": "https://your-bucket.s3.amazonaws.com/raw/capture-8812.mov?X-Amz-Signature=...",
"output": {
"format": "mp4",
"height": 720,
"fps": 30,
"video_bitrate": "2000k",
"audio_bitrate": "128k",
"faststart": true
},
"webhook_url": "https://api.yourapp.com/hooks/video-ready"
}'
The full option list and the exact webhook payload shape are in the FFmpeg Micro docs, and the same call is what you'd fire from an n8n, Make, or Zapier workflow if your pipeline lives there instead of in your backend. Once the step lives on a server, adding burned-in captions or a 9:16 crop for social is a change to a JSON body, not a new app release.
Bitrate targets that survive a phone capture
Phone captures are unusually clean sources: good lighting most of the time, a fixed sensor, and heavy in-camera denoising, which means they hold up at lower bitrates than a screen recording or game capture would. These targets are for H.264 High profile with a maxrate about 20% above the average and a bufsize of twice the average.
| Use | Resolution / fps | Video bitrate | Audio | Size per minute |
|---|---|---|---|---|
| Social and web playback | 720p30 | 2.0 Mbps | 128 kbps AAC stereo | ~16 MB |
| Higher-detail archive | 1080p30 | 4.5 Mbps | 128 kbps AAC stereo | ~35 MB |
| Motion-heavy capture | 1080p60 | 7.0 Mbps | 128 kbps AAC stereo | ~53 MB |
| Preview or feed scrub | 480p30 | 800 kbps | dropped | ~6 MB |
| Transcription only | audio only | n/a | 64 kbps mono, 16 kHz | ~0.5 MB |
That last row is the one people miss. Stripping the video track with -vn -c:a aac -b:a 64k -ac 1 -ar 16000 gets you about 52 minutes of speech under a 25 MB cap, and 16 kHz mono is what every local Whisper build wants anyway.
When on-device compression is the right call
A quick on-device pass genuinely wins in one situation: the user is on cellular and you cannot ask them to push 95 MB. Running react-native-compressor at 2 Mbps first turns that into a 16 MB upload, which is the difference between an upload that completes on a train and one that doesn't. Do that pass, then still normalize server-side, because the phone's output is one variant and you'll want three.
Skip the server entirely if the video never leaves the device, or if your app is offline-first by design and a processing round trip would break the core flow. And if what you actually need is a designed video with animated text and brand layouts rather than a normalized upload, a template-editor service fits that better than any encoding API, including this one.
Common pitfalls
Most expo-camera upload pipelines break in the same six places, usually a week into real usage.
- Keying downstream logic on the file extension. iOS hands you
.mov, Android hands you.mp4, and your storage path, content-type header, and player config all need to survive both. - Ignoring rotation metadata. Phone captures carry a display matrix, and if you stream-copy into a new container or pass
-noautorotate, you get sideways video that played fine during testing on the device. - Re-encoding with
-c:v libx264and no-movflags +faststart, which puts the moov atom at the end of the file and makes web playback wait for a full download before the first frame. - Compressing on device while the phone is still warm from a long capture, then blaming the library when the job takes 30 seconds.
- Sending the full video file to a transcription endpoint. The 25 MB cap counts the video track you don't need, and stripping it is a 0.5 MB-per-minute job.
- Deleting the raw capture before the normalized output is confirmed. Keep the original for 24 hours behind an S3 lifecycle rule so a failed job is retryable rather than a lost testimonial.
FAQ
Why are expo-camera video files so large?
expo-camera video files are large because the library records at the platform recorder's default bitrate, roughly 12.7 Mbps at 720p, which is about six times what H.264 needs for that resolution. The library exposes no setting to lower it, which is the subject of expo/expo issue #33042.
Can I set the video bitrate in expo-camera?
You cannot set a video bitrate in expo-camera. recordAsync accepts maxDuration, mute, and an iOS-only codec, none of which control bitrate or frame rate, so the only ways to reduce file size are capping duration, re-encoding on the device with a library like react-native-compressor, or re-encoding after upload.
Should I compress video before upload in React Native or after?
Compress before upload when the user is on cellular and the raw file would be 95 MB, and normalize after upload in every case. On-device compression solves the transfer cost and nothing else; server-side processing is where captions, watermarks, reframing, and multiple output variants can happen without an app release.
What bitrate should I use for a 720p phone capture?
Use 2.0 Mbps video with 128 kbps AAC audio for a 720p30 phone capture, which produces about 16 MB per minute. Phone footage is heavily denoised in-camera and holds that bitrate well; go to 4.5 Mbps only if you're keeping the file at 1080p.
Does FFmpegKit still work for React Native video compression?
FFmpegKit no longer works for React Native video compression. The project is retired, its release binaries return 404, and ffmpeg-kit-react-native can't be installed from them, which is why most current React Native guidance points either to a hardware-accelerated compressor library or to a cloud video API.
Normalizing every mobile upload is one API call, it runs in seconds on a one-minute clip, and the free tier is enough to wire the webhook end to end before you decide anything. Sign up free and point your first presigned URL at it.
About Javid Jamae
Founder & CEO at FFmpeg Micro
Javid is a software engineer, author, and entrepreneur with over 25 years of professional software development experience across enterprise, startup, and consulting environments. He founded FFmpeg Micro to make video processing accessible to developers through a simple, automation-first REST API.
You might also like

Compress Video for Discord: Hit 10, 50, or 500 MB on Purpose
Discord's limit is 10 MB free, 50 MB Nitro Basic, 500 MB Nitro. Compress video for Discord by computing bitrate from duration, then two-pass encode to hit it.

The Telegram Bot Video Size Limit Isn't 50 MB. It's Three Caps.
The telegram bot video size limit is 50 MB, or 20 MB by URL. Measure with ffprobe, compute a bitrate budget for a 48 MB ceiling, then re-encode or split.

TikTok Video Specs That Survive the Upload (and the Posting API)
TikTok video specs in one place: 1080x1920, H.264, 10 minutes, 500 MB mobile vs 4 GB web, plus the real FFmpeg preset and the Content Posting API rules.
Ready to process videos at scale?
Start using FFmpeg Micro's simple API today. No infrastructure required.
Get Started Free