@mindinventory/react-native-nitro-video
v0.1.0
Published
A high-performance React Native video processing library powered by Nitro Modules.
Downloads
170
Readme
React Native Nitro Video
A high-performance native video processing and editing library for React Native, built with Nitro Modules.
The goal is to provide a native, high-performance video editing foundation for React Native, while keeping the JavaScript API simple and moving performance-sensitive media operations to native Android and iOS implementations.
Release Status
Current version: 0.1.0
This is the first public development release.
The following capabilities are currently available on Android and iOS:
- Video session
- Metadata extraction
- Thumbnail extraction
- Timeline thumbnails
- Video playback
- Video preview
- Timeline scrubbing
- Timeline trim selection
- Native video trimming
- Native video export
- Export progress
- Export cancellation
📋 Requirements
This library requires modern React Native features for JSI bindings:
- React Native:
0.83.0or higher.- Note: The
<VideoPreview>component uses Nitro's JSI bindings to pass callbacks/references directly to Fabric. This requiresRawValueto JSI conversion support which is only available in React Native 0.83+. Running on older versions (e.g. 0.81) will trigger a native C++RawValuecast assertion crash.
- Note: The
- New Architecture: Enabled (Fabric + TurboModules).
📦 Installation
yarn add @mindinventory/react-native-nitro-video
# or
npm install @mindinventory/react-native-nitro-videoPeer Dependencies
This library depends on the following libraries which must be installed in your project:
yarn add react-native-nitro-modules react-native-worklets react-native-gesture-handler react-native-reanimated
# or
npm install react-native-nitro-modules react-native-worklets react-native-gesture-handler react-native-reanimatedEnsure you follow the installation and setup guides for each of these peer dependencies (e.g. configuring babel.config.js for Reanimated, etc.).
For iOS, go to your project's ios folder and install pods:
cd ios && pod install✨ Goals
- ⚡ High performance — expensive media operations run natively.
- 🧩 Nitro-based architecture — use HybridObjects and HybridViews for low-overhead JS ↔ native communication.
- 📱 Android + iOS — keep the public JavaScript API platform-independent.
- 🎬 Video editing foundation — build toward trimming, composition, export, and advanced editing.
- 🧠 Simple JavaScript API — React Native developers should not need to understand native media internals.
- 🔒 Explicit lifecycle management — native sessions and players have clear ownership and release paths.
- 🏗️ Incremental architecture — introduce abstractions when a real feature requires them.
📦 Architecture
The library uses Nitro HybridObjects and HybridViews as the primary native communication layer.
React Native / JavaScript
│
▼
Video API
│
┌─────┴─────┐
▼ ▼
VideoSession VideoPlayer
│ │
│ ▼
│ VideoPreview
│ │
┌───┴───┐ ┌───┴───┐
▼ ▼ ▼ ▼
Android iOS Android iOS
│ │ │ │
▼ ▼ ▼ ▼
Native AVFoundation ExoPlayer AVPlayer
Media APIs
The editor layer combines the media session, metadata, player, and timeline:
VideoSession
│
├── metadata
└── thumbnails
│
▼
ThumbnailManager
│
▼
TimelineDataSource
│
▼
VideoTimeline
VideoPlayer
│
▼
VideoEditorController
│
▼
useVideoEditor()
🧩 JavaScript API
Load a video
const session = await Video.load(videoUri);Get metadata
const metadata = await session.getMetadata();export interface VideoMetadata {
duration: number;
width: number;
height: number;
rotation: number;
frameRate: number;
bitrate: number;
codec: string;
fileSize: number;
}Extract a thumbnail
const thumbnail = await session.getThumbnail(5000);Resize natively:
const thumbnail = await session.getThumbnail(5000, {
width: 320,
height: 180,
});The output is a JPEG-encoded Nitro ArrayBuffer.
Generate timeline thumbnails
const thumbnails = await session.getThumbnails({
startTimeMs: 0,
endTimeMs: 10000,
intervalMs: 1000,
width: 160,
height: 90,
});export interface VideoThumbnail {
timeMs: number;
data: ArrayBuffer;
}ThumbnailManager
const manager = new ThumbnailManager(session, {
intervalMs: 1000,
width: 160,
height: 90,
cacheWindowMs: 30000,
maxCacheItems: 300,
});await manager.getRange(0, 10000);
await manager.getVisibleRange({
startTimeMs: 0,
endTimeMs: 10000,
prefetchMs: 5000,
});Video player
const player = await Video.createPlayer(videoUri);
await player.play();
await player.pause();
await player.seek(5000);
const currentTime = await player.getCurrentTime();
const isPlaying = await player.isPlaying();
await player.release();The player exposes native position updates:
player.addPositionListener((timeMs) => {
console.log('Position:', timeMs);
});
player.removePositionListener();Video preview
VideoPreview is a Nitro HybridView backed by PlayerView on Android and AVPlayerLayer on iOS.
\<VideoPreview
hybridRef={callback((ref) => {
previewRef.current = ref;
})}
style={{
width: '100%',
height: 240,
}}
/>
The player is attached using the HybridView methods:
previewRef.current?.attachPlayer(player);
previewRef.current?.detachPlayer();Video timeline
VideoTimeline is the reusable timeline component for playback scrubbing and trim-range selection.
Modes
type VideoTimelineMode = 'scrub' | 'trim';The mode can be switched at runtime without recreating the timeline.
Public API
interface VideoTimelineProps {
session: VideoSession;
durationMs: number;
width: number;
currentTimeMs?: number;
mode?: VideoTimelineMode;
disabled?: boolean;
trimRange?: VideoTimeRange;
minTrimDurationMs?: number;
thumbnailWidth?: number;
thumbnailHeight?: number;
intervalMs?: number;
prefetchMs?: number;
onTrimRangeChange?: (range: VideoTimeRange) => void;
onTimeChange?: (timeMs: number) => void;
onScrub?: (timeMs: number) => void;
onScrubStart?: () => void;
onScrubEnd?: () => void;
onTrimInteractionStart?: () => void;
onTrimInteractionEnd?: () => void;
}Scrub mode
<VideoTimeline
session={session}
durationMs={metadata.duration}
currentTimeMs={state.currentTimeMs}
mode="scrub"
onScrub={seek}
/>Trim mode
const [trimRange, setTrimRange] = useState({
startTimeMs: 0,
endTimeMs: metadata.duration,
});
<VideoTimeline
session={session}
durationMs={metadata.duration}
currentTimeMs={state.currentTimeMs}
mode="trim"
trimRange={trimRange}
minTrimDurationMs={500}
onTrimRangeChange={setTrimRange}
onScrub={seek}
/>;Trim mode provides draggable start/end handles, selected-range feedback, minimum-range enforcement, and edge auto-scroll.
The trim range is controlled by the consumer:
interface VideoTimeRange {
startTimeMs: number;
endTimeMs: number;
}disabled prevents timeline scrubbing and trim interaction while keeping the timeline visible.
Video editor
The editor combines session, metadata, player state, playback errors, and timeline behavior:
const { state, seek, play, pause, togglePlayback, retry } = useVideoEditor(
session,
metadata,
player
);Current state:
export interface VideoEditorState {
durationMs: number;
currentTimeMs: number;
isPlaying: boolean;
error: VideoPlaybackError | null;
}The editor handles playback, pause, seeking, current-position synchronization, timeline scrubbing, playback-driven timeline movement, playback errors, and manual playback retry.
The actual trim operation remains on the native session:
const result = await session.trim({
startTimeMs: 5000,
endTimeMs: 12000,
});Video export
To export a trimmed video segment, use session.exportVideo(options). This performs a native background export task:
const task = await session.exportVideo({
startTimeMs: 1000, // optional
endTimeMs: 5000, // optional
});
// Track export progress
task.addProgressListener((event) => {
console.log(
`Export progress: ${event.progress * 100}%, status: ${event.status}`
);
});
// Wait for the result or catch any failures / cancellations
try {
const result = await task.getResult();
console.log('Export finished. Saved to:', result.outputUri);
} catch (error) {
if (error.code === 'CANCELLED') {
console.log('Export was cancelled.');
} else {
console.error('Export failed:', error.message);
}
}
// To cancel the export task programmatically:
// await task.cancel();Export types
export type VideoExportStatus =
| 'preparing'
| 'exporting'
| 'completed'
| 'cancelled'
| 'failed';
export interface VideoExportProgress {
progress: number;
status: VideoExportStatus;
}
export interface VideoExportResult {
outputUri: string;
}
export type VideoExportErrorCode =
| 'INVALID_TIME_RANGE'
| 'SOURCE_NOT_FOUND'
| 'EXPORT_FAILED'
| 'CANCELLED'
| 'OUTPUT_FAILED';
---
# 🔌 Nitro Specifications
## VideoModule
```ts
export interface VideoModule extends HybridObject<{
ios: 'swift';
android: 'kotlin';
}> {
load(source: string): Promise<VideoSession>;
createPlayer(source: string): Promise<VideoPlayer>;
}VideoSession
export interface VideoSession extends HybridObject<{
ios: 'swift';
android: 'kotlin';
}> {
getMetadata(): Promise<VideoMetadata>;
getThumbnail(
timeMs: number,
options?: ThumbnailOptions
): Promise<ArrayBuffer>;
getThumbnails(request: ThumbnailRequest): Promise<VideoThumbnail[]>;
trim(options: VideoTrimOptions): Promise<VideoTrimResult>;
exportVideo(options?: VideoExportOptions): Promise<VideoExportTask>;
release(): Promise<void>;
}VideoPlayer
export interface VideoPlayer extends HybridObject<{
ios: 'swift';
android: 'kotlin';
}> {
play(): Promise<void>;
pause(): Promise<void>;
seek(timeMs: number): Promise<void>;
getCurrentTime(): Promise<number>;
isPlaying(): Promise<boolean>;
addPositionListener(listener: (timeMs: number) => void): void;
removePositionListener(): void;
addErrorListener(listener: (error: VideoPlaybackError) => void): void;
removeErrorListener(): void;
retry(): Promise<void>;
release(): Promise<void>;
}VideoExportTask
export interface VideoExportTask extends HybridObject<{
ios: 'swift';
android: 'kotlin';
}> {
getResult(): Promise<VideoExportResult>;
cancel(): Promise<void>;
addProgressListener(listener: (progress: VideoExportProgress) => void): void;
removeProgressListener(): void;
}VideoPreview
export interface VideoPreviewProps extends HybridViewProps {}
export interface VideoPreviewMethods extends HybridViewMethods {
attachPlayer(player: VideoPlayer): void;
detachPlayer(): void;
}
export type VideoPreview = HybridView<
VideoPreviewProps,
VideoPreviewMethods,
{
ios: 'swift';
android: 'kotlin';
}
>;The view is implemented in Kotlin on Android and Swift on iOS.
🏗️ Native Architecture
The library delegates performance-sensitive media operations to native platform APIs via Nitro:
- Android: Uses Jetpack Media3 (
ExoPlayer,PlayerView) for video playback/preview, Jetpack Media3Transformerfor trimming and export tasks, andMediaMetadataRetrieverfor metadata and frame (thumbnail) extraction. - iOS: Uses AVFoundation (
AVPlayer,AVPlayerLayer,AVAssetImageGenerator,AVAssetExportSession) for playback, metadata, frame extraction, trimming, and export tasks.
📁 Project Structure
src/
├── video.ts
├── index.tsx
├── VideoPreview.tsx
├── specs/
│ ├── VideoExportTask.nitro.ts
│ ├── VideoModule.nitro.ts
│ ├── VideoPlayer.nitro.ts
│ ├── VideoPreview.nitro.ts
│ └── VideoSession.nitro.ts
├── types/
│ ├── error.ts
│ ├── export.ts
│ ├── metadata.ts
│ ├── playback.ts
│ ├── thumb.ts
│ ├── timelines.ts
│ └── trim.ts
├── timeline/
│ ├── ThumbnailManager.ts
│ ├── TimelineDataSource.ts
│ ├── VideoTimeline.tsx
│ └── thumbnailToUri.ts
└── editor/
├── VideoEditorController.ts
├── useVideoEditor.ts
└── validateTrim.ts
android/
└── src/main/java/com/margelo/nitro/nitrovideo/
├── NitroVideo.kt
├── NitroVideoPackage.kt
├── HybridVideoSession.kt
├── HybridVideoPlayer.kt
├── HybridVideoPreview.kt
├── codec/
│ └── VideoDecoderResolver.kt
├── export/
│ ├── HybridVideoExportTask.kt
│ └── VideoExportException.kt
├── metadata/
│ └── MetadataExtractor.kt
├── thumbnail/
│ └── ThumbnailExtractor.kt
└── trim/
└── TrimExtractor.kt
ios/
├── HybridVideoExportTask.swift
├── HybridVideoPlayer.swift
├── HybridVideoPreview.swift
├── HybridVideoSession.swift
├── NitroVideo.swift
├── Metadata/
│ ├── MetadataExtractor.swift
│ └── VideoMetadataError.swift
├── thumbnail/
│ └── ThumbnailExtractor.swift
└── trim/
└── VideoTrimmer.swift
nitrogen/
└── generated/
├── android/
└── ios/Generated files under nitrogen/generated/ must not be edited manually.
The TypeScript Nitro specifications are the source of truth.
🗺️ Roadmap
The development progress and feature goals of the library:
✅ Completed Capabilities
- Core Video Session: Loading video sources, lifecycle management (
Video.load(),VideoSession.release()). - Metadata Extraction: Access to video dimensions, rotation, codec, bitrate, frame rate, and file size.
- Thumbnail System: Frame-accurate single and batch thumbnail extraction at custom intervals, cached locally using an LRU cache.
- Video Playback & Preview: Native
VideoPlayerand<VideoPreview>UI components with playback positioning listeners. - Video Trimming: Native frame-accurate trimming of videos generating a new output file.
- Video Export: Native export task with progress monitoring and cancellation support.
🔜 Future Plans (Roadmap)
- Multi-Clip Editing & Composition: Support for merging, transitions, and native rendering of multiple clips.
- Soundtrack / Audio System: Native background audio tracks addition, volume control, and mixing.
- Advanced Editing Features: Overlays (text, images), filters, and speed adjustments.
⚡ Performance Principles
Native-first media processing
JavaScript
│
│ commands / configuration
▼
Nitro
│
▼
Native media engine
JavaScript should not repeatedly process decoded video frames.
Avoid unnecessary data copying
Large media data should not unnecessarily travel through:
Native → C++ → JS → Native
Only data required by the JavaScript API should cross the boundary.
Native thumbnail processing
Native frame
↓
Native resize
↓
JPEG encoding
↓
Nitro ArrayBuffer
↓
JavaScript
Asynchronous operations
Operations involving disk I/O, decoding, encoding, frame extraction, and export should not block the React Native JavaScript thread.
Explicit resource lifecycle
const player = await Video.createPlayer(uri);
try {
await player.play();
} finally {
await player.release();
}🧪 Example
A minimal editor flow:
const session = await Video.load(uri);
const metadata = await session.getMetadata();
const player = await Video.createPlayer(uri);
const editor = useVideoEditor(session, metadata, player);In React, the resulting state can drive the preview, timeline, and controls:
\<VideoPreview
hybridRef={callback((ref) => {
previewRef.current = ref;
})}
style={{
width: '100%',
height: 240,
}}
/>
\<VideoTimeline
session={session}
durationMs={metadata.duration}
currentTimeMs={state.currentTimeMs}
onScrub={seek}
/>
🛠️ Development
The example application is used to validate the native implementation.
TypeScript API
↓
Nitrogen generation
↓
Android / iOS native implementation
↓
Example application
↓
Device / simulator testing
When changing:
src/specs/*.nitro.ts
run:
yarn nitrogen
before rebuilding the example application.
Generated files should never be edited manually.
📋 API Design Rules
Time
All public video timeline values use milliseconds.
5000; // 5 secondsFile size
Bytes.
Bitrate
Bits per second.
Dimensions
Pixels.
Frame rate
Frames per second.
Image output
Thumbnail/frame extraction currently returns JPEG encoded as a Nitro ArrayBuffer.
🚧 Current Limitations
The project is currently in an early public release stage.
Currently:
Thumbnail batch cancellation and progress callbacks are not yet exposed.
Dedicated native playback-state events (
playing,paused,buffering,ended) are not yet exposed.Background playback/interruption handling is not yet a dedicated abstraction.
Remote video source support is not yet defined as a single cross-platform contract.
Multi-clip composition is not implemented yet.
Advanced editing operations are not implemented yet.
APIs may change before the first stable release.
🗺️ Roadmap
React Native Nitro Video
│
▼
┌─────────────────┐
│ Video Session │
│ ✅ │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Metadata │
│ ✅ │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Thumbnails │
│ ✅ │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Timeline / │
│ Batch Thumbnails│
│ ✅ │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Preview / │
│ Playback │
│ ✅ │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Trim │
│ ✅ │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Export │
│ ✅ │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Multi-Clip │
│ Editing │
│ 🔜 │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Advanced Editor │
│ Features │
│ 🔜 │
└─────────────────┘🤝 Development Philosophy
This project prioritizes measured architecture over premature abstraction.
New abstractions should be introduced when a real feature requires them.
The architecture intentionally avoids introducing large abstractions such as:
VideoSource
SessionManager
EditingGraph
TimelineEngine
ExportManageruntil their responsibilities become necessary.
The objective is to keep the core implementation understandable while allowing the architecture to evolve toward a complete native video editing engine.
📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
