Skip to content

Latest commit

Β 

History

15 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

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

🎬 Video Preview

Simulator.Screen.Recording.-.iPhone.15.Pro.-.2026-08-26.at.11.52.20.1.mp4


πŸ“‹ Requirements

This library requires modern React Native features for JSI bindings:

  • React Native: 0.83.0 or higher.
    • Note: The <VideoPreview> component uses Nitro's JSI bindings to pass callbacks/references directly to Fabric. This requires RawValue to 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++ RawValue cast assertion crash.
  • New Architecture: Enabled (Fabric + TurboModules).

πŸ“¦ Installation

yarn add @mindinventory/react-native-nitro-video
# or
npm install @mindinventory/react-native-nitro-video

Peer 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-reanimated

Ensure 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 Media3 Transformer for trimming and export tasks, and MediaMetadataRetriever for 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 VideoPlayer and <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 seconds

File 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
ExportManager

until 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.

Releases

Packages

Contributors

Languages