Skip to content

thangdevalone/react-native-media-toolkit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

68 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

react-native-media-toolkit

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.

npm license

     


Compatibility

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 16.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.
Use expo run:ios or expo run:android instead.


Features

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.


Installation

npm install react-native-media-toolkit react-native-nitro-modules
# or
yarn add react-native-media-toolkit react-native-nitro-modules

iOS:

cd ios && pod install

Android: No extra steps. Gradle resolves Media3 automatically.


Usage

import { MediaToolkit } from 'react-native-media-toolkit';

Crop image

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);

Compress image

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'
});

Split image into grid

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);

Flip image

const result = await MediaToolkit.flipImage(imageUri, {
  direction: 'horizontal', // 'horizontal' | 'vertical'
});

Rotate image

const result = await MediaToolkit.rotateImage(imageUri, {
  degrees: 90, // 90, 180, 270
});

Process image (Multi-transform)

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,
});

Trim video

const result = await MediaToolkit.trimVideo(videoUri, {
  startTime: 2000,  // start in milliseconds
  endTime: 7000,    // end in milliseconds
});

Concatenate videos (no re-encode)

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.

Crop video

const result = await MediaToolkit.cropVideo(videoUri, {
  x: 0.1,
  y: 0.1,
  width: 0.8,
  height: 0.8,
});

Trim + Crop in one pass

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.

Compress video

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 (default 720)
  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, and bitrate are all optional — but the library needs at least one signal to determine bitrate. If you pass nothing, it defaults to quality: 'medium' (~4 Mbps). targetSizeInMB takes highest priority; bitrate overrides quality.

Extract thumbnail

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 ms

Flip video

const result = await MediaToolkit.flipVideo(videoUri, {
  direction: 'horizontal', // 'horizontal' | 'vertical'
});

Rotate video

const result = await MediaToolkit.rotateVideo(videoUri, {
  degrees: 90, // 90, 180, 270
});

Process video (Multi-transform)

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,
});

Change video speed

const result = await MediaToolkit.changeVideoSpeed(videoUri, {
  speed: 2.0, // 0.25x to 4.0x
});

Extract audio

Extracts the audio track from a video and saves it as an m4a file.

const audio = await MediaToolkit.extractAudio(videoUri, {});
// audio.uri -> file:///.../audio.m4a

Generate video preview (GIF)

Generates 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
});

API Reference

Convention: Required = must be provided. Optional = has a sensible default or can be omitted entirely.

cropImage(uri, options): Promise<MediaResult>

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.

compressImage(uri, options): Promise<MediaResult>

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.

splitImage(uri, options): Promise<MediaResult[]>

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

flipImage(uri, options): Promise<MediaResult>

flipVideo(uri, options): Promise<MediaResult>

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.

rotateImage(uri, options): Promise<MediaResult>

rotateVideo(uri, options): Promise<MediaResult>

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.

processImage(uri, options): Promise<MediaResult>

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
outputPath string Absolute path for the output file. Defaults to a temp file.

trimVideo(uri, options): Promise<MediaResult>

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.

concatVideos(clipPaths, outputPath): Promise<ConcatResult>

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.

cropVideo(uri, options): Promise<MediaResult>

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.

trimAndCropVideo(uri, options): Promise<MediaResult>

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.

processVideo(uri, options): Promise<MediaResult>

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
outputPath string Absolute path for the output file. Defaults to a temp file.

compressVideo(uri, options): Promise<MediaResult>

All options are optional. The bitrate strategy follows this priority:

  1. targetSizeInMB → smart-compress: calculates optimal bitrate and resolution from duration + target size (highest priority)
  2. bitrate → explicit bitrate override (takes priority over quality)
  3. 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 720 Optional. Minimum short-edge resolution (px) when using targetSizeInMB. Prevents over-downscaling.
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.
outputPath string temp file Optional. Absolute path for the output file.

getThumbnail(uri, options?): Promise<ThumbnailResult>

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

changeVideoSpeed(uri, options): Promise<MediaResult>

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.

extractAudio(uri, options): Promise<MediaResult>

Option Type Default Description
outputPath string temp file Optional. Absolute path for the output .m4a file.

generateVideoPreview(uri, options): Promise<MediaResult>

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.

getMediaMetadata(uri): Promise<MediaMetadata>

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.

Return types

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'
}

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, and duration in ThumbnailResult refer to the source video metadata — not the thumbnail image. This makes getThumbnail a lightweight way to probe video metadata without processing the file.


Custom UI

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 strip
  • CropOverlay — a draggable crop box with corner resize handles

You can copy these components directly into your project and adapt them as needed.


Performance

This library is designed around two principles: avoid unnecessary work and stay on the native thread.

No bridge overhead

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.

Trim without re-encoding

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.

Single-pass trim + crop

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.

Memory-Efficient Image Processing (OOM-Free)

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.inSampleSize to subsample the image during the hardware decoding phase, bypassing full-resolution memory allocation entirely.
  • iOS: Uses CGImageSourceCreateThumbnailAtIndex to instruct ImageIO to decode and downscale directly from the file descriptor buffer.

Smart Video Compression

The compressVideo API provides a dynamically balanced encoding strategy via the targetSizeInMB flag. When provided, the library will:

  • Calculate a bounded target bitrate mapped by the duration of the media track.
  • Adjust the output resolution dynamically, floor-bounded by minResolution to maintain pixel clarity at constrained bitrates.
  • Optionally strip the audio track (muteAudio) to allocate the entire output bandwidth to the visual presentation.

Comparison with common alternatives

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

Architecture

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)

Contributing

See CONTRIBUTING.md

License

MIT — see LICENSE

Author

thangdevalonequangthangvtlg@gmail.com
GitHub: https://github.com/thangdevalone

About

A lightweight and high-performance media processing library for React Native, built on top of Nitro Modules (JSI).

Topics

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors