ffmpeg-kit-react-native is dead. Here is a native iOS alternative.
If you build video export into a React Native iOS app, you have probably already hit the wall.
ffmpeg-kit-react-native was the default answer for years. In late 2023 the upstream ffmpeg-kit project was archived, and every release binary on GitHub now returns 404. A dependency that cannot download its own binaries is not a dependency you can ship.
So people reach for the alternatives, and each one fails a different way.
| Option | Why it does not work |
|---|---|
ffmpeg-kit-react-native |
Archived. All release binaries 404. |
| Other RN FFmpeg wrappers | They depend on the same dead arthenica binaries. |
| FFmpeg compiled to WASM | Does not run on Hermes, React Native's JS engine. |
| Server-side encoding | Needs a network round-trip, adds latency, and sends your users' frames to a server, which is a privacy problem for a lot of apps. |
| Writing AVFoundation yourself | Correct answer, but it is a few hundred lines of Swift most JS teams do not want to write. |
The last row is the point. Apple ships a fully capable, hardware-accelerated H.264 encoder in every iPhone and iPad: AVFoundation. It has been stable since iOS 4, it runs on the device's video encoder chip, and it never touches the network. The only reason people were reaching for FFmpeg at all is that nobody had wrapped AVFoundation in a React Native module they could just install.
So I wrote one: expo-video-encoder.
What it is
An Expo native module that takes a sequence of JPEG frames and encodes them into an H.264 MP4, on device, using AVFoundation. No external binaries. No xcframework download. No server. No app.json plugin. You install it and Expo autolinking wires up the native module.
npm install expo-video-encoder
npx expo prebuild
The minimum viable export
import * as FileSystem from 'expo-file-system';
import * as MediaLibrary from 'expo-media-library';
import { encodeVideo } from 'expo-video-encoder';
async function exportVideo() {
const framesDir = `${FileSystem.cacheDirectory}frames/`;
const outputPath = `${FileSystem.cacheDirectory}output.mp4`;
await FileSystem.makeDirectoryAsync(framesDir, { intermediates: true });
// write your frames as frame_000000.jpg, frame_000001.jpg, ...
await encodeVideo({
framesDir: framesDir.replace(/^file:\/\//, ''),
frameCount: 60,
fps: 30,
width: 1920,
height: 1080,
outputPath: outputPath.replace(/^file:\/\//, ''),
});
await MediaLibrary.createAssetAsync(outputPath);
}
That is the whole thing. You hand it a directory of numbered JPEGs and it hands you an MP4.
Where the frames come from
Most people encoding video in React Native are recording a canvas: a Skia drawing, an animation, a chart. @shopify/react-native-skia can snapshot to JPEG base64 directly, so the capture loop is short:
import { useCanvasRef } from '@shopify/react-native-skia';
const ref = useCanvasRef();
for (let i = 0; i < totalFrames; i++) {
seekTo(i / fps); // move your animation to frame i
await new Promise(r => requestAnimationFrame(r)); // let Skia render
const image = await ref.current?.makeImageSnapshotAsync();
const base64 = image!.encodeToBase64(); // JPEG by default
const name = `frame_${String(i).padStart(6, '0')}.jpg`;
await FileSystem.writeAsStringAsync(`${framesDir}${name}`, base64, {
encoding: FileSystem.EncodingType.Base64,
});
}
Why JPEG frames instead of raw pixels? Because raw RGBA arrays are four to ten times larger to push across the JS bridge, JPEG decode on iOS is hardware-accelerated, and every canvas library already knows how to produce JPEG base64. It is the pragmatic transfer format between JS and native.
How it actually works
Under the hood the pipeline is standard AVFoundation:
- Each JPEG is decoded into a
UIImage, drawn into aCVPixelBufferviaCGContext, and appended to anAVAssetWriterInputPixelBufferAdaptorat its presentation timestamp (frame_index / fps). - The
AVAssetWritersession stays open across all frames, then finalizes withmarkAsFinished()andfinishWriting(). - Audio, if you add it, goes through an
AVMutableComposition: each track is inserted at a millisecond offset and the whole thing is exported withAVAssetExportSession, which handles resampling and mixing.
None of that is novel. That is the point. This is the boring, correct, Apple-blessed way to encode video on iOS, packaged so a JS team never has to open Xcode.
The honest limitation
It is iOS only. AVFoundation is an Apple framework; there is no Android in this package today. Calling it on a non-iOS platform throws a clear error, so guard your call:
if (Platform.OS === 'ios') {
await encodeVideo({ /* ... */ });
}
If your app is Android-first, this will not solve your problem yet. Android's equivalent is MediaCodec, and a cross-platform version is the obvious next step. But for the very large number of teams whose export feature only ever needed to work on iPhone, this unblocks you today.
Why this matters
The FFmpeg-on-mobile story was always a little strange: shipping a large, general-purpose transcoding library to do something the operating system already does in hardware. Now that the FFmpeg binaries are gone, it is a good moment to stop fighting the platform and use the encoder Apple already gave you.
If you are the person who just found the 404 on the ffmpeg-kit release page: install expo-video-encoder, wire your frame loop, and move on with your day.
- Package: github.com/ajibadedapo/expo-video-encoder
- npm:
expo-video-encoder - MIT licensed
It is early and I would genuinely value bug reports and, especially, help on the Android (MediaCodec) side.
Top comments (0)