Read this in: Tiếng Việt
Native image and video processing for React Native — crop, trim, compress, and thumbnail extraction.
Built on Nitro Modules (JSI), using AVFoundation on iOS and Jetpack Media3 Transformer on Android. No FFmpeg dependency.
| Environment | Support |
|---|---|
| React Native CLI (New Architecture) | Supported |
| Expo with Dev Client / Custom Build | Supported |
| Expo Go | Not supported (requires native build) |
| React Native | 0.75+ (New Architecture required) |
| iOS | 15.0+ |
| Android | API 24+ (Android 7.0) / Android 15 (16 KB Page Size) Ready |
Expo note: This library requires a native build. It cannot run in Expo Go.
Useexpo run:iosorexpo run:androidinstead.
| Feature | iOS | Android |
|---|---|---|
| Crop image | AVFoundation / CGImage | Bitmap |
| Compress image | CGImageSource (OOM-free) | BitmapFactory / inSampleSize |
| Flip / Rotate image | CGImage / CoreGraphics | Bitmap |
| Multi-transform image | CGImage / CoreGraphics | Bitmap |
| Trim video (start/end in ms) | AVAssetExportSession | Media3 Transformer |
| Crop video (relative region) | AVMutableVideoComposition | Media3 Presentation |
| Flip / Rotate video | AVMutableVideoComposition | Media3 Presentation |
| Trim + Crop in single pass | AVMutableVideoComposition | Media3 Transformer |
| Multi-transform video | AVMutableVideoComposition | Media3 Transformer |
| Compress video | AVAssetExportSession presets | Media3 Transformer |
| Extract thumbnail from video | AVAssetImageGenerator | MediaMetadataRetriever |
All crop coordinates use a relative (0.0–1.0) system — independent of screen resolution.
npm install react-native-media-toolkit react-native-nitro-modules
# or
yarn add react-native-media-toolkit react-native-nitro-modulesiOS:
cd ios && pod installAndroid: No extra steps. Gradle resolves Media3 automatically.
import { MediaToolkit } from 'react-native-media-toolkit';const result = await MediaToolkit.cropImage(imageUri, {
x: 0.25, // required — left offset relative to image width (0.0–1.0)
y: 0.25, // required — top offset relative to image height (0.0–1.0)
width: 0.5, // required — crop width relative to image width (0.0–1.0)
height: 0.5, // required — crop height relative to image height (0.0–1.0)
outputPath: '/custom/path/out.jpg', // optional
});
console.log(result.uri, result.width, result.height);const result = await MediaToolkit.compressImage(imageUri, {
quality: 70, // optional — 0–100, default 80
maxWidth: 1080, // optional — max output width, aspect ratio preserved
maxHeight: 1920, // optional — max output height, aspect ratio preserved
format: 'jpeg', // optional — 'jpeg' | 'png' | 'webp', default 'jpeg'
});const parts = await MediaToolkit.splitImage(imageUri, {
rows: 3,
columns: 4,
quality: 100, // optional, defaults to 100 for lossy formats
});
console.log(parts.length); // 12
console.log(parts[0].uri, parts[0].width, parts[0].height);const result = await MediaToolkit.flipImage(imageUri, {
direction: 'horizontal', // 'horizontal' | 'vertical'
});const result = await MediaToolkit.rotateImage(imageUri, {
degrees: 90, // 90, 180, 270
});Run multiple operations in a single pass to save processing time and memory.
const result = await MediaToolkit.processImage(imageUri, {
cropX: 0.1,
cropY: 0.1,
cropWidth: 0.8,
cropHeight: 0.8,
flip: 'horizontal',
rotation: 90,
lutUri: 'file:///.../hald_lut_64.png', // Custom HALD LUT color grading
});const result = await MediaToolkit.trimVideo(videoUri, {
startTime: 2000, // start in milliseconds
endTime: 7000, // end in milliseconds
});const { durationSec } = await MediaToolkit.concatVideos(
[clip1Path, clip2Path, clip3Path], // absolute local file paths
outputPath // absolute output path
);Joins multiple clips into one file via AVFoundation passthrough on iOS and Media3
passthrough Transformer on Android — no decode/encode pass. Handles iOS HEVC clips
that the FFmpeg concat demuxer cannot. Audio tracks are carried through if all
clips have audio; if any clip lacks an audio track, audio is stripped from all clips
so the output has a consistent track layout.
iOS: All input clips must share the same orientation (portrait or landscape). Mixing orientations produces a misrotated output — rotate clips to a consistent orientation before concatenating.
const result = await MediaToolkit.cropVideo(videoUri, {
x: 0.1,
y: 0.1,
width: 0.8,
height: 0.8,
});const result = await MediaToolkit.trimAndCropVideo(videoUri, {
startTime: 1000,
endTime: 8000,
x: 0.0,
y: 0.1,
width: 1.0,
height: 0.8,
});Faster than running trim and crop separately — only one encode pass.
The compressor supports two modes. Use one of them:
Mode 1 — Smart compress to a target file size (recommended):
const result = await MediaToolkit.compressVideo(videoUri, {
targetSizeInMB: 8, // required for this mode — target output size in MB
minResolution: 480, // optional — minimum short-edge resolution (auto-calculated if omitted)
startTime: 2000, // optional — trim range in ms, encoded in the same pass
endTime: 7000,
muteAudio: false, // optional — strip audio track (default false)
width: 1280, // optional — max output width, aspect ratio preserved
});Mode 2 — Quality preset or explicit bitrate:
const result = await MediaToolkit.compressVideo(videoUri, {
quality: 'medium', // optional — 'low' | 'medium' | 'high' (default 'medium')
bitrate: 2_000_000, // optional — explicit bitrate in bps (overrides quality)
muteAudio: false, // optional — strip audio track (default false)
width: 1280, // optional — max output width, aspect ratio preserved
});Note:
targetSizeInMB,quality, andbitrateare all optional — but the library needs at least one signal to determine bitrate. If you pass nothing, it defaults toquality: 'medium'(~4 Mbps).targetSizeInMBtakes highest priority;bitrateoverridesquality.
const thumb = await MediaToolkit.getThumbnail(videoUri, {
timeMs: 3000, // frame time in milliseconds, default 0
quality: 85, // 0–100, default 80
maxWidth: 720, // max thumbnail width (does not affect returned metadata)
});
// thumb.uri → thumbnail JPEG file
// thumb.width → source video width (rotation-corrected)
// thumb.height → source video height
// thumb.size → source video file size in bytes
// thumb.duration → source video duration in msconst result = await MediaToolkit.flipVideo(videoUri, {
direction: 'horizontal', // 'horizontal' | 'vertical'
});const result = await MediaToolkit.rotateVideo(videoUri, {
degrees: 90, // 90, 180, 270
});Run multiple video operations in a single pass (trim, crop, flip, rotate).
const result = await MediaToolkit.processVideo(videoUri, {
startTime: 1000,
endTime: 8000,
cropX: 0.1,
cropY: 0.1,
cropWidth: 0.8,
cropHeight: 0.8,
flip: 'horizontal',
rotation: 90,
lutUri: 'http://.../hald_lut_64.png', // Custom HALD LUT color grading
});const result = await MediaToolkit.changeVideoSpeed(videoUri, {
speed: 2.0, // 0.25x to 4.0x
});Extracts the audio track from a video and saves it as an m4a file.
const audio = await MediaToolkit.extractAudio(videoUri, {});
// audio.uri -> file:///.../audio.m4aGenerates an animated GIF preview from the video frames natively without FFmpeg.
const preview = await MediaToolkit.generateVideoPreview(videoUri, {
durationMs: 3000, // Duration to capture (default 3000ms)
fps: 5, // Frames per second (default 5)
maxWidth: 0, // 0 = full source width; set e.g. 320 to downscale
});Convention:
Required= must be provided.Optional= has a sensible default or can be omitted entirely.
| Option | Type | Required | Description |
|---|---|---|---|
x |
number |
Required | Left offset relative to image width (0.0–1.0) |
y |
number |
Required | Top offset relative to image height (0.0–1.0) |
width |
number |
Required | Crop width relative to image width (0.0–1.0) |
height |
number |
Required | Crop height relative to image height (0.0–1.0) |
outputPath |
string |
Optional | Absolute path for the output file. Defaults to a temp file. |
cornerRadius |
number | string |
Optional | Corner radius in px or percentage (e.g. "50%"). 100% creates a perfect circle. |
All options are optional. Pass an empty object {} to use all defaults.
| Option | Type | Default | Description |
|---|---|---|---|
quality |
number |
80 |
JPEG/WebP encode quality (0–100) |
maxWidth |
number |
original | Max output width in px (aspect ratio preserved) |
maxHeight |
number |
original | Max output height in px (aspect ratio preserved) |
format |
string |
'jpeg' |
Output format: 'jpeg' | 'png' | 'webp' |
outputPath |
string |
temp file | Absolute path for the output file |
cornerRadius |
number | string |
0 |
Corner radius in px or percentage (e.g. "50%"). 100% creates a perfect circle. |
Splits the source image into a rows x columns grid using original pixel dimensions. Tiles are not resized. When format is omitted, the native layer keeps the source format when supported; iOS falls back to PNG for unsupported source encoders such as WebP.
| Option | Type | Required | Description |
|---|---|---|---|
rows |
number |
Required | Number of output rows |
columns |
number |
Required | Number of output columns |
format |
string |
Optional | Force output format: 'jpeg' | 'png' | 'webp' |
quality |
number |
Optional | Lossy encode quality (0–100). Defaults to 100 |
outputDir |
string |
Optional | Absolute output directory for generated tiles |
prefix |
string |
Optional | Filename prefix for generated tiles |
| Option | Type | Required | Description |
|---|---|---|---|
direction |
string |
Required | 'horizontal' or 'vertical' |
outputPath |
string |
Optional | Absolute path for the output file. Defaults to a temp file. |
cornerRadius |
number | string |
Optional | Corner radius in px or percentage (e.g. "50%"). 100% creates a perfect circle. |
| Option | Type | Required | Description |
|---|---|---|---|
degrees |
number |
Required | 90, 180, or 270 |
outputPath |
string |
Optional | Absolute path for the output file. Defaults to a temp file. |
cornerRadius |
number | string |
Optional | Corner radius in px or percentage (e.g. "50%"). 100% creates a perfect circle. |
Multi-transform image in a single pass. All options are optional.
| Option | Type | Description |
|---|---|---|
cropX |
number |
Crop left offset relative to image width (0.0–1.0) |
cropY |
number |
Crop top offset relative to image height (0.0–1.0) |
cropWidth |
number |
Crop width relative to image width (0.0–1.0) |
cropHeight |
number |
Crop height relative to image height (0.0–1.0) |
flip |
string |
'horizontal' or 'vertical' |
rotation |
number |
90, 180, or 270 |
lutUri |
string |
URI to a 64x64x64 HALD LUT PNG image (512x512) for custom color grading. Supports local, asset, and remote links. |
outputPath |
string |
Absolute path for the output file. Defaults to a temp file. |
| Option | Type | Required | Description |
|---|---|---|---|
startTime |
number |
Required | Trim start position in milliseconds |
endTime |
number |
Required | Trim end position in milliseconds |
outputPath |
string |
Optional | Absolute path for the output file. Defaults to a temp file. |
cornerRadius |
number | string |
Optional | Corner radius in px or percentage (e.g. "50%"). 100% creates a perfect circle. |
Joins clips in order without re-encoding. iOS uses AVAssetExportPresetPassthrough
over an AVMutableComposition; Android uses a Media3 Composition of
EditedMediaItemSequence with no effects (Transformer auto-selects the passthrough
muxer). ConcatResult.durationSec is the sum of input durations in seconds.
Audio: All clips must have a consistent track layout. If any clip lacks an audio track, audio is stripped from all clips in the output.
iOS orientation: All input clips must share the same orientation. Mixing portrait and landscape clips produces a misrotated result — normalize orientations before concatenating.
| Argument | Type | Required | Description |
|---|---|---|---|
clipPaths |
string[] |
Required | Absolute local file paths (mov/mp4) to concatenate in order |
outputPath |
string |
Required | Absolute output path. Will be overwritten if it exists. The parent directory is created automatically if it does not exist. |
Same relative coordinate system as cropImage — all values in the range (0.0–1.0).
| Option | Type | Required | Description |
|---|---|---|---|
x |
number |
Required | Left offset relative to frame width (0.0–1.0) |
y |
number |
Required | Top offset relative to frame height (0.0–1.0) |
width |
number |
Required | Crop width relative to frame width (0.0–1.0) |
height |
number |
Required | Crop height relative to frame height (0.0–1.0) |
outputPath |
string |
Optional | Absolute path for the output file. Defaults to a temp file. |
cornerRadius |
number | string |
Optional | Corner radius in px or percentage (e.g. "50%"). 100% creates a perfect circle. |
Combines trim and crop into a single encode pass — faster and avoids double-encode quality loss.
| Option | Type | Required | Description |
|---|---|---|---|
startTime |
number |
Required | Trim start position in milliseconds |
endTime |
number |
Required | Trim end position in milliseconds |
x |
number |
Required | Crop left offset relative to frame width (0.0–1.0) |
y |
number |
Required | Crop top offset relative to frame height (0.0–1.0) |
width |
number |
Required | Crop width relative to frame width (0.0–1.0) |
height |
number |
Required | Crop height relative to frame height (0.0–1.0) |
outputPath |
string |
Optional | Absolute path for the output file. Defaults to a temp file. |
cornerRadius |
number | string |
Optional | Corner radius in px or percentage (e.g. "50%"). 100% creates a perfect circle. |
Multi-transform video in a single pass (trim, crop, flip, rotate). All options are optional.
| Option | Type | Description |
|---|---|---|
startTime |
number |
Trim start position in milliseconds |
endTime |
number |
Trim end position in milliseconds |
cropX |
number |
Crop left offset relative to frame width (0.0–1.0) |
cropY |
number |
Crop top offset relative to frame height (0.0–1.0) |
cropWidth |
number |
Crop width relative to frame width (0.0–1.0) |
cropHeight |
number |
Crop height relative to frame height (0.0–1.0) |
flip |
string |
'horizontal' or 'vertical' |
rotation |
number |
90, 180, or 270 |
lutUri |
string |
URI to a 64x64x64 HALD LUT PNG image (512x512) for custom color grading. Supports local, asset, and remote links. |
outputPath |
string |
Absolute path for the output file. Defaults to a temp file. |
All options are optional. The bitrate strategy follows this priority:
targetSizeInMB→ smart-compress: calculates optimal bitrate and resolution from duration + target size (highest priority)bitrate→ explicit bitrate override (takes priority overquality)quality→ preset mapping:low≈ 1 Mbps ·medium≈ 4 Mbps ·high≈ 8 Mbps (default)
If none of the three are passed, the library falls back to quality: 'medium'.
| Option | Type | Default | Description |
|---|---|---|---|
targetSizeInMB |
number |
— | Optional. Target output file size in MB. When set, overrides quality and bitrate. |
minResolution |
number |
auto | Optional. Minimum short-edge resolution (px) when using targetSizeInMB. Prevents over-downscaling. When omitted, auto-calculates a safe floor based on the source resolution (≈33% of original, min 240p). |
quality |
string |
'medium' |
Optional. Preset: 'low' | 'medium' | 'high'. Ignored if targetSizeInMB or bitrate is set. |
bitrate |
number |
— | Optional. Explicit target bitrate in bps. Overrides quality; ignored if targetSizeInMB is set. |
width |
number |
original | Optional. Max output width in px (aspect ratio preserved). |
muteAudio |
boolean |
false |
Optional. Strip audio track from the output. |
startTime |
number |
0 |
Optional. Trim start in ms. Compresses only [startTime, endTime] in the same encode pass. Must be finite and nonnegative. |
endTime |
number |
video end | Optional. Trim end in ms. Must be finite, greater than startTime, and within the video duration. An explicit 0 is invalid. |
outputPath |
string |
temp file | Optional. Absolute path for the output file. |
When a trim range is given, targetSizeInMB budgets the trimmed duration (not the whole file), the "target must be smaller than the original" check is skipped since the range's size is only an estimate, and invalid ranges reject with invalidInput before any session starts. Ranges must span at least one millisecond; omitted bounds default to the start/end, while negative, non-finite, reversed, empty, or out-of-bounds values are rejected. A trimmed export is never replaced with the full source if encoding increases its size, including the explicit-bitrate iOS path. Prefer this over trimVideo followed by compressVideo: a trim that does not start on a keyframe is a full re-encode at the source resolution on Android, so the two-step path encodes twice.
options itself is optional — pass nothing to extract a full-res JPEG at time 0.
| Option | Type | Default | Description |
|---|---|---|---|
timeMs |
number |
0 |
Frame time in milliseconds |
quality |
number |
80 |
JPEG output quality (0–100) |
maxWidth |
number |
original | Max thumbnail width in px (aspect ratio preserved) |
outputPath |
string |
temp file | Absolute path for the output JPEG |
| Option | Type | Default | Description |
|---|---|---|---|
speed |
number |
Required | Speed multiplier (e.g. 0.5 for half speed, 2.0 for double speed). Supported range: 0.25 to 4.0. |
outputPath |
string |
temp file | Optional. Absolute path for the output file. |
| Option | Type | Default | Description |
|---|---|---|---|
outputPath |
string |
temp file | Optional. Absolute path for the output .m4a file. |
Generates an animated GIF natively.
| Option | Type | Default | Description |
|---|---|---|---|
fps |
number |
5 |
Optional. Frames per second for the preview. |
durationMs |
number |
3000 |
Optional. Duration in milliseconds to capture from the start. |
maxWidth |
number |
0 |
Optional. Maximum width of the preview (aspect ratio preserved). Use 0 for full source size. |
quality |
number |
80 |
Optional. Quality 0-100 (mapped to internal encoder settings). |
outputPath |
string |
temp file | Optional. Absolute path for the output .gif file. |
Gets unified metadata directly from the native source. For images, extracts deep EXIF/TIFF/GPS data. For videos, extracts track sizes, location, and creation info natively without bloated parsing.
- No options required. It automatically sniffs the file type.
interface MediaResult {
uri: string; // file:// URI of the output file
size: number; // file size in bytes
width: number; // output width in pixels
height: number; // output height in pixels
duration: number; // duration in ms (0 for images)
mime: string; // MIME type, e.g. 'video/mp4'
encoder?: string; // Android video exports: '<default|software_encoder>:<codec>', see below
}
interface MediaMetadata {
type: string; // 'image' | 'video'
width: number;
height: number;
size: number;
duration: number; // 0 for images
mime: string;
make?: string; // Camera Make (e.g. Apple)
model?: string; // Camera Model
datetime?: string;
location?: { latitude: number; longitude: number };
// EXIF specific (Images only)
aperture?: number;
exposureTime?: number;
iso?: number;
focalLength?: number;
}
interface ThumbnailResult {
uri: string; // file:// URI of the output JPEG thumbnail
size: number; // source video file size in bytes
width: number; // source video width in pixels (rotation-corrected)
height: number; // source video height in pixels (rotation-corrected)
duration: number; // source video duration in milliseconds
}Note:
width,height,size, anddurationinThumbnailResultrefer to the source video metadata — not the thumbnail image. This makesgetThumbnaila lightweight way to probe video metadata without processing the file.
react-native-media-toolkit directly supports ph://<localIdentifier> URIs on iOS across all video and image operations (trimVideo, cropVideo, compressVideo, concatVideos, getThumbnail, getMediaMetadata, compressImage, splitImage, etc.).
On iOS, raw filesystem paths inside /var/mobile/Media/DCIM/... (which some picker libraries like expo-media-library expose as localUri, sometimes tagged with base64 fragments #...) cannot be directly decoded by a fresh AVURLAsset in an application process due to Apple's system security sandbox (tracksError = The file couldn't be opened because you don't have permission to view it).
To work with videos and photos from the device library:
- Use
ph://<localIdentifier>: Pass the asset identifier directly (e.g.ph://ED7AC36B-A150-4C38-BB8C-B6D696F69B2C/L0/001orph://<id>). The toolkit resolves the underlying asset via PhotoKit (PHImageManager.requestAVAsset/requestImageDataAndOrientation). Ensure yourInfo.plistincludesNSPhotoLibraryUsageDescription. - Or copy into the app sandbox: If your integration only provides raw DCIM file paths, copy the file into your app's sandbox directory (e.g.
cacheDirectory/documentDirectory) first before processing. (Libraries likeexpo-image-pickerperform this copy automatically).
This library is headless — it provides native processing logic only, without any built-in UI.
You are free to build any trim timeline, crop overlay, or progress indicator that fits your app design.
The example app (example/src/App.tsx) includes reference implementations of:
VideoTrimBar— a timeline scrubber with dual handles and thumbnail stripCropOverlay— a draggable crop box with corner resize handles
You can copy these components directly into your project and adapt them as needed.
This library is designed around two principles: avoid unnecessary work and stay on the native thread.
All API calls go through JSI (JavaScript Interface) via Nitro Modules. There is no JSON serialization between JS and native — the call is a direct C++ function call. This eliminates the main bottleneck of the old Bridge architecture.
trimVideo uses AVAssetExportPresetPassthrough on iOS and a keyframe-aligned cut on Android. The compressed bitstream is copied as-is — no decode, no re-encode. A 30-second video trims in under 1 second regardless of resolution.
Running trimVideo then cropVideo sequentially means two full encode passes: decode → encode → decode → encode. trimAndCropVideo does both in one session: decode → encode once. This halves the processing time and avoids quality loss from double encoding.
Standard image processing operations can cause Out-Of-Memory (OOM) exceptions when decoding high-resolution images (e.g., 40MP+). To prevent this, the library handles decoding via Load-Time Downsampling:
- Android: Utilizes
BitmapFactory.Options.inSampleSizeto subsample the image during the hardware decoding phase, bypassing full-resolution memory allocation entirely. - iOS: Uses
CGImageSourceCreateThumbnailAtIndexto instructImageIOto decode and downscale directly from the file descriptor buffer.
Every video export tries Media3's default (hardware) encoder first. If the video encoder fails, at initialisation or mid-export (some devices reject a clip on the first frame, e.g. OMX.qcom.video.encoder.avc on a Samsung SM-A526B with OMX_ErrorHardware), the export runs once more with a software encoder before failing. MediaResult.encoder reports which one produced the file; a failure's message lists each attempt with Media3's error code and the MediaCodec.CodecException details, and logcat tag VideoProcessor carries the same.
The compressVideo API provides a dynamically balanced encoding strategy via the targetSizeInMB flag. When provided, the library will:
- Calculate a bounded target
bitratemapped by thedurationof the media track. - Adjust the output resolution dynamically, floor-bounded by
minResolution(or an auto-calculated safe floor when omitted) to maintain pixel clarity at constrained bitrates. - Optionally strip the audio track (
muteAudio) to allocate the entire output bandwidth to the visual presentation.
| Library | Native Engine | JS Bridge | Image Support | Video Support | Trim (no re-encode) | Multi-transform (1-pass) |
|---|---|---|---|---|---|---|
| react-native-media-toolkit | AVFoundation / Media3 | JSI (Nitro) | Yes (OOM-free) | Yes | Yes | Yes |
react-native-compressor |
Custom AVFoundation / MediaCodec | Bridge | Yes | Yes | No | No |
react-native-video-trim |
FFmpegKit (+30MB size) | Bridge | No | Yes (UI included) | Yes | Yes |
react-native-media-toolkit/
├── src/
│ ├── MediaToolkit.nitro.ts Nitro HybridObject spec + TypeScript types
│ └── index.ts Public JS/TS API
├── ios/
│ ├── HybridMediaToolkit.swift Nitro entry point (Swift)
│ ├── ImageProcessor.swift CGImage crop and compress
│ ├── VideoProcessor.swift AVFoundation trim, crop, compress, thumbnail
│ └── MediaToolkitErrors.swift Error definitions
├── android/
│ └── src/main/java/com/mediatoolkit/
│ ├── HybridMediaToolkit.kt Nitro entry point (Kotlin)
│ ├── ImageProcessor.kt Bitmap crop and compress
│ ├── VideoProcessor.kt Media3 trim, crop, compress, thumbnail
│ └── MediaToolkitException.kt Error definitions
├── nitrogen/ Generated C++ and Swift/Kotlin bridge files
└── example/ Demo app (Expo Dev Client)
See CONTRIBUTING.md
MIT — see LICENSE
thangdevalone — quangthangvtlg@gmail.com
GitHub: https://github.com/thangdevalone


