DEV Community

specvista technologies
specvista technologies

Posted on

ffmpeg-kit-react-native is dead. Here is a native iOS alternative.

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
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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,
  });
}
Enter fullscreen mode Exit fullscreen mode

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 a CVPixelBuffer via CGContext, and appended to an AVAssetWriterInputPixelBufferAdaptor at its presentation timestamp (frame_index / fps).
  • The AVAssetWriter session stays open across all frames, then finalizes with markAsFinished() and finishWriting().
  • Audio, if you add it, goes through an AVMutableComposition: each track is inserted at a millisecond offset and the whole thing is exported with AVAssetExportSession, 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({ /* ... */ });
}
Enter fullscreen mode Exit fullscreen mode

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.

It is early and I would genuinely value bug reports and, especially, help on the Android (MediaCodec) side.

Top comments (0)