DEV Community

Muhammad Faeiz Furqan
Muhammad Faeiz Furqan

Posted on

How to Compress Videos in Expo Without FFmpeg (2026 Guide)

If you've ever tried compressing videos in a React Native app, you've probably hit the same wall: FFmpeg. It works, but it adds ~9 MB to your APK, requires complex command-line arguments, and runs entirely in software — burning through battery and taking forever on longer videos.

There's a better way. Modern phones ship with dedicated hardware encoders (MediaCodec on Android, VideoToolbox on iOS) that can compress video 50x faster than FFmpeg while using a fraction of the battery. The problem? Accessing them from React Native has historically required writing native code.

Not anymore.

In this tutorial, I'll show you how to compress videos (and images) in Expo using hardware acceleration — no FFmpeg, no ejecting, no native code. Whether you're building a chat app, social media platform, or any app that handles user-generated media — this approach will save you bandwidth, storage, and headaches.


What You'll Learn

  • How to compress videos with H.264 and HEVC hardware encoding
  • How to compress images with quality and size control
  • How to add progress bars and cancellation to your compression flow
  • How to handle background compression on iOS
  • A production-ready upload flow you can copy into your app

The Problem with FFmpeg in Mobile Apps

Before we dive in, let's understand why FFmpeg is a poor fit for mobile:

Issue Impact
APK size Adds ~9 MB to your binary
Speed Software encoding is 10-50x slower than hardware
Battery CPU-intensive, drains battery fast
API complexity You're writing shell commands as strings
No progress tracking Hard to show users a progress bar

Hardware encoders solve all of these. They're built into every phone's SoC, they're fast, and they're power-efficient. The challenge is just accessing them from JavaScript — which is what expo-image-and-video-compressor does.


Prerequisites

  • React Native project with Expo SDK 51+
  • Basic familiarity with TypeScript/JavaScript
  • A physical device or emulator for testing (compression uses hardware encoders)

Setup

Install the package:

npx expo install expo-image-and-video-compressor
Enter fullscreen mode Exit fullscreen mode

That's it. No config plugins, no expo prebuild, no ejecting. It works in both managed and bare Expo workflows because it's built as a true Expo Module.

Coming from react-native-compressor? Unlike react-native-compressor, this package doesn't require config plugins or expo-dev-client. It works out of the box with expo install.


Step 1: Basic Video Compression

Let's start with the simplest case — compress a video to a reasonable size for uploading:

import { compress } from 'expo-image-and-video-compressor';

async function compressVideo(uri: string) {
  const compressedUri = await compress(uri, {
    maxSize: 1080,
    bitrate: 2_500_000,
    codec: 'h264',
    speed: 'ultrafast',
  });

  return compressedUri;
}
Enter fullscreen mode Exit fullscreen mode

What's happening here:

  • maxSize: 1080 — scales the video so the largest dimension is 1080px (aspect ratio preserved)
  • bitrate: 2_500_000 — targets 2.5 Mbps (good quality for social/chat apps)
  • codec: 'h264' — uses H.264, which every device supports
  • speed: 'ultrafast' — prioritizes encoding speed

A 4K, 30-second video (~85 MB) typically compresses to ~8 MB in about 3 seconds. That's hardware acceleration at work.


Step 2: Adding a Progress Bar

Users hate staring at a spinner. Let's add real-time progress:

import { useState } from 'react';
import { View, Text, Button } from 'react-native';
import { compress } from 'expo-image-and-video-compressor';

function VideoUploader({ videoUri }: { videoUri: string }) {
  const [progress, setProgress] = useState(0);
  const [compressing, setCompressing] = useState(false);

  async function handleCompress() {
    setCompressing(true);
    setProgress(0);

    const result = await compress(videoUri, {
      maxSize: 720,
      bitrate: 2_000_000,
      codec: 'h264',
      speed: 'fast',
    }, (p) => {
      setProgress(Math.round(p * 100));
    });

    setCompressing(false);
    console.log('Compressed file:', result);
    // Upload `result` to your server
  }

  return (
    <View style={{ padding: 20 }}>
      <Button title="Compress Video" onPress={handleCompress} disabled={compressing} />
      {compressing && (
        <View style={{ marginTop: 10 }}>
          <Text>Compressing: {progress}%</Text>
          <View style={{
            height: 8,
            backgroundColor: '#e0e0e0',
            borderRadius: 4,
            marginTop: 8,
          }}>
            <View style={{
              height: 8,
              backgroundColor: '#4CAF50',
              borderRadius: 4,
              width: `${progress}%`,
            }} />
          </View>
        </View>
      )}
    </View>
  );
}
Enter fullscreen mode Exit fullscreen mode

The third argument to compress() is a callback that fires with a value from 0.0 to 1.0 as the encoder processes each frame. We multiply by 100 to get a percentage.


Step 3: Letting Users Cancel

Large videos can take a while. Give users an escape hatch:

import { compress, cancel } from 'expo-image-and-video-compressor';

let cancellationId: string;

// Start compression
const promise = compress(videoUri, {
  maxSize: 1080,
  getCancellationId: (id) => {
    cancellationId = id;
  },
}, (progress) => {
  console.log(`${Math.round(progress * 100)}%`);
});

// Cancel button handler
function onCancelPress() {
  if (cancellationId) {
    cancel(cancellationId);
  }
}

// Handle the result
try {
  const result = await promise;
  console.log('Compressed:', result);
} catch (error) {
  console.log('Compression cancelled or failed');
}
Enter fullscreen mode Exit fullscreen mode

When you call cancel(), the promise returned by compress() will reject, so always wrap it in a try/catch.


Step 4: Using HEVC for Even Smaller Files

HEVC (H.265) produces ~40% smaller files at the same visual quality compared to H.264. If you're building a chat app or social platform where storage and bandwidth costs matter, this is a big deal:

const result = await compress(videoUri, {
  codec: 'hevc',      // ~40% smaller than h264 at same quality
  maxSize: 1080,
  speed: 'fast',
});
Enter fullscreen mode Exit fullscreen mode

Compatibility: HEVC encoding requires iOS 11+ and Android 5.0+ (API 21+), which covers virtually every device in active use today.

When to use which codec:

Codec File Size Compatibility Best For
h264 Baseline Universal Maximum device support
hevc ~40% smaller iOS 11+, Android 5+ Chat apps, social platforms, bandwidth savings

Step 5: Image Compression

The same package handles images too — no need for a separate library:

import { compressImage } from 'expo-image-and-video-compressor';

const result = await compressImage(imageUri, {
  maxWidth: 1080,
  maxHeight: 1080,
  quality: 0.8,        // JPEG quality (0.0 - 1.0)
  output: 'jpg',       // or 'png'
});
Enter fullscreen mode Exit fullscreen mode

It handles orientation correction and EXIF preservation automatically — no more rotated photos after compression. If the compressed image is somehow larger than the original, it returns the original URI instead.


Step 6: Getting Video Metadata

Before compressing, you might want to check if compression is even needed:

import { getMetadata } from 'expo-image-and-video-compressor';

const meta = await getMetadata(videoUri);
console.log(meta);
// {
//   width: 1920,
//   height: 1080,
//   duration: 30.5,      // seconds
//   size: 89400000,      // bytes
//   bitrate: 23000000,   // bps
//   extension: 'mp4'
// }

const sizeMB = meta.size / 1_000_000;
if (sizeMB < 5) {
  console.log('Video is already small enough, skip compression');
}
Enter fullscreen mode Exit fullscreen mode

Putting It All Together: Production Upload Flow

Here's a realistic example combining everything — metadata check, conditional compression, background support, and upload:

import {
  compress,
  compressImage,
  getMetadata,
  activateBackgroundTask,
  deactivateBackgroundTask,
} from 'expo-image-and-video-compressor';

async function prepareAndUpload(
  uri: string,
  type: 'video' | 'image',
  onProgress?: (p: number) => void,
) {
  let compressedUri = uri;

  if (type === 'video') {
    // Check if compression is even needed
    const meta = await getMetadata(uri);
    const sizeMB = meta.size / 1_000_000;
    console.log(`Original: ${sizeMB.toFixed(1)} MB, ${meta.width}x${meta.height}`);

    if (sizeMB > 5) {
      // Keep compression alive in background (iOS)
      await activateBackgroundTask(() => {
        console.warn('Background time expiring');
      });

      compressedUri = await compress(uri, {
        maxSize: 720,
        bitrate: 2_000_000,
        codec: 'hevc',
        speed: 'fast',
      }, onProgress);

      await deactivateBackgroundTask();

      const newMeta = await getMetadata(compressedUri);
      console.log(`Compressed: ${(newMeta.size / 1_000_000).toFixed(1)} MB`);
    }
  } else {
    compressedUri = await compressImage(uri, {
      maxWidth: 1080,
      maxHeight: 1080,
      quality: 0.75,
    });
  }

  // Upload compressedUri to your API
  await uploadToServer(compressedUri);
}
Enter fullscreen mode Exit fullscreen mode

Key details:

  • getMetadata() — check the file size before deciding to compress
  • activateBackgroundTask() — prevents iOS from killing your app mid-compression if the user switches apps
  • HEVC + 720p — a great default for chat/social uploads (small files, good quality)
  • Skip small files — if the video is already under 5 MB, don't waste time compressing

Speed Presets Explained

The speed option controls the trade-off between encoding speed and compression ratio:

Preset Speed File Size Best For
ultrafast Fastest Larger Quick previews, real-time UX
fast Moderate Medium General purpose uploads
balanced Slowest Smallest Storage-sensitive apps, background processing

For most apps, ultrafast or fast is the right choice. The file size difference between presets is typically 10-20%, while the speed difference can be 3-5x.


Common Recipes

Chat App (WhatsApp-style)

await compress(videoUri, {
  maxSize: 720,
  bitrate: 1_500_000,
  codec: 'hevc',
  speed: 'fast',
});
Enter fullscreen mode Exit fullscreen mode

Social Media Post

await compress(videoUri, {
  maxSize: 1080,
  bitrate: 3_000_000,
  codec: 'h264',
  speed: 'ultrafast',
});
Enter fullscreen mode Exit fullscreen mode

Profile Photo

await compressImage(imageUri, {
  maxWidth: 500,
  maxHeight: 500,
  quality: 0.85,
});
Enter fullscreen mode Exit fullscreen mode

Thumbnail Generation

await compressImage(imageUri, {
  maxWidth: 200,
  maxHeight: 200,
  quality: 0.6,
});
Enter fullscreen mode Exit fullscreen mode

Wrapping Up

You don't need FFmpeg to compress videos in React Native. Hardware encoders exist on every phone — you just need a way to access them from JavaScript.

expo-image-and-video-compressor gives you:

  • H.264 and HEVC hardware encoding
  • Image compression with EXIF preservation
  • Real-time progress tracking and cancellation
  • iOS background task support
  • Zero native code setup in Expo managed workflow
  • ~50 KB APK impact (vs FFmpeg's ~9 MB)

Install it and try it out:

npx expo install expo-image-and-video-compressor
Enter fullscreen mode Exit fullscreen mode

If this helped you, give the repo a star — it helps other developers find it. Drop a comment below if you have questions or run into issues. I read every one.

Follow me for more React Native and Expo content.

Top comments (0)