{"id":47770615,"url":"https://github.com/thangdevalone/react-native-media-toolkit","last_synced_at":"2026-04-28T17:01:10.946Z","repository":{"id":348762647,"uuid":"1199739045","full_name":"thangdevalone/react-native-media-toolkit","owner":"thangdevalone","description":"A lightweight and high-performance media processing library for React Native, built on top of Nitro Modules (JSI).","archived":false,"fork":false,"pushed_at":"2026-04-23T18:35:01.000Z","size":5543,"stargazers_count":41,"open_issues_count":0,"forks_count":2,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-23T19:10:32.714Z","etag":null,"topics":["media","react-native","tool-kit"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/react-native-media-toolkit","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/thangdevalone.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-02T16:53:40.000Z","updated_at":"2026-04-23T18:35:05.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/thangdevalone/react-native-media-toolkit","commit_stats":null,"previous_names":["thangdevalone/react-native-media-toolkit"],"tags_count":11,"template":false,"template_full_name":null,"purl":"pkg:github/thangdevalone/react-native-media-toolkit","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thangdevalone%2Freact-native-media-toolkit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thangdevalone%2Freact-native-media-toolkit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thangdevalone%2Freact-native-media-toolkit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thangdevalone%2Freact-native-media-toolkit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/thangdevalone","download_url":"https://codeload.github.com/thangdevalone/react-native-media-toolkit/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thangdevalone%2Freact-native-media-toolkit/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32390067,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-28T14:34:11.604Z","status":"ssl_error","status_checked_at":"2026-04-28T14:32:37.009Z","response_time":56,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["media","react-native","tool-kit"],"created_at":"2026-04-03T09:03:47.426Z","updated_at":"2026-04-28T17:01:10.940Z","avatar_url":"https://github.com/thangdevalone.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# react-native-media-toolkit\n\nRead this in: [Tiếng Việt](./README.vi.md)\n\n---\n\nNative image and video processing for React Native — crop, trim, compress, and thumbnail extraction.  \nBuilt on **Nitro Modules** (JSI), using `AVFoundation` on iOS and **Jetpack Media3 Transformer** on Android. No FFmpeg dependency.\n\n[![npm](https://img.shields.io/npm/v/react-native-media-toolkit)](https://www.npmjs.com/package/react-native-media-toolkit)\n[![license](https://img.shields.io/npm/l/react-native-media-toolkit)](LICENSE)\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\".github/images/1.png\" width=\"200\" /\u003e\n  \u0026nbsp;\u0026nbsp;\n  \u003cimg src=\".github/images/2.png\" width=\"200\" /\u003e\n  \u0026nbsp;\u0026nbsp;\n  \u003cimg src=\".github/images/3.png\" width=\"200\" /\u003e\n\u003c/p\u003e\n\n---\n\n## Compatibility\n\n| Environment | Support |\n|---|---|\n| React Native CLI (New Architecture) | Supported |\n| Expo with Dev Client / Custom Build | Supported |\n| Expo Go | Not supported (requires native build) |\n| React Native | 0.75+ (New Architecture required) |\n| iOS | 15.1+ |\n| Android | API 24+ (Android 7.0) |\n\n\u003e **Expo note:** This library requires a native build. It cannot run in Expo Go.  \n\u003e Use `expo run:ios` or `expo run:android` instead.\n\n---\n\n## Features\n\n| Feature | iOS | Android |\n|---|---|---|\n| Crop image | AVFoundation / CGImage | Bitmap |\n| Compress image | CGImageSource (OOM-free) | BitmapFactory / inSampleSize |\n| Flip / Rotate image | CGImage / CoreGraphics | Bitmap |\n| Multi-transform image | CGImage / CoreGraphics | Bitmap |\n| Trim video (start/end in ms) | AVAssetExportSession | Media3 Transformer |\n| Crop video (relative region) | AVMutableVideoComposition | Media3 Presentation |\n| Flip / Rotate video | AVMutableVideoComposition | Media3 Presentation |\n| Trim + Crop in single pass | AVMutableVideoComposition | Media3 Transformer |\n| Multi-transform video | AVMutableVideoComposition | Media3 Transformer |\n| Compress video | AVAssetExportSession presets | Media3 Transformer |\n| Extract thumbnail from video | AVAssetImageGenerator | MediaMetadataRetriever |\n\nAll crop coordinates use a **relative (0.0–1.0) system** — independent of screen resolution.\n\n---\n\n## Installation\n\n```sh\nnpm install react-native-media-toolkit react-native-nitro-modules\n# or\nyarn add react-native-media-toolkit react-native-nitro-modules\n```\n\n**iOS:**\n```sh\ncd ios \u0026\u0026 pod install\n```\n\n**Android:** No extra steps. Gradle resolves Media3 automatically.\n\n---\n\n## Usage\n\n```typescript\nimport { MediaToolkit } from 'react-native-media-toolkit';\n```\n\n### Crop image\n\n```typescript\nconst result = await MediaToolkit.cropImage(imageUri, {\n  x: 0.25,      // required — left offset relative to image width (0.0–1.0)\n  y: 0.25,      // required — top offset relative to image height (0.0–1.0)\n  width: 0.5,   // required — crop width relative to image width (0.0–1.0)\n  height: 0.5,  // required — crop height relative to image height (0.0–1.0)\n  outputPath: '/custom/path/out.jpg', // optional\n});\nconsole.log(result.uri, result.width, result.height);\n```\n\n### Compress image\n\n```typescript\nconst result = await MediaToolkit.compressImage(imageUri, {\n  quality: 70,       // optional — 0–100, default 80\n  maxWidth: 1080,    // optional — max output width, aspect ratio preserved\n  maxHeight: 1920,   // optional — max output height, aspect ratio preserved\n  format: 'jpeg',    // optional — 'jpeg' | 'png' | 'webp', default 'jpeg'\n});\n```\n\n### Flip image\n\n```typescript\nconst result = await MediaToolkit.flipImage(imageUri, {\n  direction: 'horizontal', // 'horizontal' | 'vertical'\n});\n```\n\n### Rotate image\n\n```typescript\nconst result = await MediaToolkit.rotateImage(imageUri, {\n  degrees: 90, // 90, 180, 270\n});\n```\n\n### Process image (Multi-transform)\n\nRun multiple operations in a single pass to save processing time and memory.\n```typescript\nconst result = await MediaToolkit.processImage(imageUri, {\n  cropX: 0.1,\n  cropY: 0.1,\n  cropWidth: 0.8,\n  cropHeight: 0.8,\n  flip: 'horizontal',\n  rotation: 90,\n});\n```\n\n### Trim video\n\n```typescript\nconst result = await MediaToolkit.trimVideo(videoUri, {\n  startTime: 2000,  // start in milliseconds\n  endTime: 7000,    // end in milliseconds\n});\n```\n\n### Crop video\n\n```typescript\nconst result = await MediaToolkit.cropVideo(videoUri, {\n  x: 0.1,\n  y: 0.1,\n  width: 0.8,\n  height: 0.8,\n});\n```\n\n### Trim + Crop in one pass\n\n```typescript\nconst result = await MediaToolkit.trimAndCropVideo(videoUri, {\n  startTime: 1000,\n  endTime: 8000,\n  x: 0.0,\n  y: 0.1,\n  width: 1.0,\n  height: 0.8,\n});\n```\n\n\u003e Faster than running trim and crop separately — only one encode pass.\n\n### Compress video\n\nThe compressor supports two modes. Use **one** of them:\n\n**Mode 1 — Smart compress to a target file size** (recommended):\n```typescript\nconst result = await MediaToolkit.compressVideo(videoUri, {\n  targetSizeInMB: 8,   // required for this mode — target output size in MB\n  minResolution: 480,  // optional — minimum short-edge resolution (default 720)\n  muteAudio: false,    // optional — strip audio track (default false)\n  width: 1280,         // optional — max output width, aspect ratio preserved\n});\n```\n\n**Mode 2 — Quality preset or explicit bitrate**:\n```typescript\nconst result = await MediaToolkit.compressVideo(videoUri, {\n  quality: 'medium',   // optional — 'low' | 'medium' | 'high' (default 'medium')\n  bitrate: 2_000_000,  // optional — explicit bitrate in bps (overrides quality)\n  muteAudio: false,    // optional — strip audio track (default false)\n  width: 1280,         // optional — max output width, aspect ratio preserved\n});\n```\n\n\u003e **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`.\n\n### Extract thumbnail\n\n```typescript\nconst thumb = await MediaToolkit.getThumbnail(videoUri, {\n  timeMs: 3000,    // frame time in milliseconds, default 0\n  quality: 85,     // 0–100, default 80\n  maxWidth: 720,   // max thumbnail width (does not affect returned metadata)\n});\n// thumb.uri      → thumbnail JPEG file\n// thumb.width    → source video width (rotation-corrected)\n// thumb.height   → source video height\n// thumb.size     → source video file size in bytes\n// thumb.duration → source video duration in ms\n```\n\n### Flip video\n\n```typescript\nconst result = await MediaToolkit.flipVideo(videoUri, {\n  direction: 'horizontal', // 'horizontal' | 'vertical'\n});\n```\n\n### Rotate video\n\n```typescript\nconst result = await MediaToolkit.rotateVideo(videoUri, {\n  degrees: 90, // 90, 180, 270\n});\n```\n\n### Process video (Multi-transform)\n\nRun multiple video operations in a single pass (trim, crop, flip, rotate).\n```typescript\nconst result = await MediaToolkit.processVideo(videoUri, {\n  startTime: 1000,\n  endTime: 8000,\n  cropX: 0.1,\n  cropY: 0.1,\n  cropWidth: 0.8,\n  cropHeight: 0.8,\n  flip: 'horizontal',\n  rotation: 90,\n});\n```\n\n---\n\n## API Reference\n\n\u003e **Convention:** `Required` = must be provided. `Optional` = has a sensible default or can be omitted entirely.\n\n### `cropImage(uri, options): Promise\u003cMediaResult\u003e`\n\n| Option | Type | Required | Description |\n|---|---|---|---|\n| `x` | `number` | **Required** | Left offset relative to image width (0.0–1.0) |\n| `y` | `number` | **Required** | Top offset relative to image height (0.0–1.0) |\n| `width` | `number` | **Required** | Crop width relative to image width (0.0–1.0) |\n| `height` | `number` | **Required** | Crop height relative to image height (0.0–1.0) |\n| `outputPath` | `string` | Optional | Absolute path for the output file. Defaults to a temp file. |\n\n### `compressImage(uri, options): Promise\u003cMediaResult\u003e`\n\nAll options are optional. Pass an empty object `{}` to use all defaults.\n\n| Option | Type | Default | Description |\n|---|---|---|---|\n| `quality` | `number` | `80` | JPEG/WebP encode quality (0–100) |\n| `maxWidth` | `number` | original | Max output width in px (aspect ratio preserved) |\n| `maxHeight` | `number` | original | Max output height in px (aspect ratio preserved) |\n| `format` | `string` | `'jpeg'` | Output format: `'jpeg'` \\| `'png'` \\| `'webp'` |\n| `outputPath` | `string` | temp file | Absolute path for the output file |\n\n### `flipImage(uri, options): Promise\u003cMediaResult\u003e`\n### `flipVideo(uri, options): Promise\u003cMediaResult\u003e`\n\n| Option | Type | Required | Description |\n|---|---|---|---|\n| `direction` | `string` | **Required** | `'horizontal'` or `'vertical'` |\n| `outputPath` | `string` | Optional | Absolute path for the output file. Defaults to a temp file. |\n\n### `rotateImage(uri, options): Promise\u003cMediaResult\u003e`\n### `rotateVideo(uri, options): Promise\u003cMediaResult\u003e`\n\n| Option | Type | Required | Description |\n|---|---|---|---|\n| `degrees` | `number` | **Required** | `90`, `180`, or `270` |\n| `outputPath` | `string` | Optional | Absolute path for the output file. Defaults to a temp file. |\n\n### `processImage(uri, options): Promise\u003cMediaResult\u003e`\n\nMulti-transform image in a single pass. All options are **optional**.\n\n| Option | Type | Description |\n|---|---|---|\n| `cropX` | `number` | Crop left offset relative to image width (0.0–1.0) |\n| `cropY` | `number` | Crop top offset relative to image height (0.0–1.0) |\n| `cropWidth` | `number` | Crop width relative to image width (0.0–1.0) |\n| `cropHeight` | `number` | Crop height relative to image height (0.0–1.0) |\n| `flip` | `string` | `'horizontal'` or `'vertical'` |\n| `rotation` | `number` | `90`, `180`, or `270` |\n| `outputPath` | `string` | Absolute path for the output file. Defaults to a temp file. |\n\n### `trimVideo(uri, options): Promise\u003cMediaResult\u003e`\n\n| Option | Type | Required | Description |\n|---|---|---|---|\n| `startTime` | `number` | **Required** | Trim start position in milliseconds |\n| `endTime` | `number` | **Required** | Trim end position in milliseconds |\n| `outputPath` | `string` | Optional | Absolute path for the output file. Defaults to a temp file. |\n\n### `cropVideo(uri, options): Promise\u003cMediaResult\u003e`\n\nSame relative coordinate system as `cropImage` — all values in the range (0.0–1.0).\n\n| Option | Type | Required | Description |\n|---|---|---|---|\n| `x` | `number` | **Required** | Left offset relative to frame width (0.0–1.0) |\n| `y` | `number` | **Required** | Top offset relative to frame height (0.0–1.0) |\n| `width` | `number` | **Required** | Crop width relative to frame width (0.0–1.0) |\n| `height` | `number` | **Required** | Crop height relative to frame height (0.0–1.0) |\n| `outputPath` | `string` | Optional | Absolute path for the output file. Defaults to a temp file. |\n\n### `trimAndCropVideo(uri, options): Promise\u003cMediaResult\u003e`\n\nCombines trim and crop into a **single encode pass** — faster and avoids double-encode quality loss.\n\n| Option | Type | Required | Description |\n|---|---|---|---|\n| `startTime` | `number` | **Required** | Trim start position in milliseconds |\n| `endTime` | `number` | **Required** | Trim end position in milliseconds |\n| `x` | `number` | **Required** | Crop left offset relative to frame width (0.0–1.0) |\n| `y` | `number` | **Required** | Crop top offset relative to frame height (0.0–1.0) |\n| `width` | `number` | **Required** | Crop width relative to frame width (0.0–1.0) |\n| `height` | `number` | **Required** | Crop height relative to frame height (0.0–1.0) |\n| `outputPath` | `string` | Optional | Absolute path for the output file. Defaults to a temp file. |\n\n### `processVideo(uri, options): Promise\u003cMediaResult\u003e`\n\nMulti-transform video in a single pass (trim, crop, flip, rotate). All options are **optional**.\n\n| Option | Type | Description |\n|---|---|---|\n| `startTime` | `number` | Trim start position in milliseconds |\n| `endTime` | `number` | Trim end position in milliseconds |\n| `cropX` | `number` | Crop left offset relative to frame width (0.0–1.0) |\n| `cropY` | `number` | Crop top offset relative to frame height (0.0–1.0) |\n| `cropWidth` | `number` | Crop width relative to frame width (0.0–1.0) |\n| `cropHeight` | `number` | Crop height relative to frame height (0.0–1.0) |\n| `flip` | `string` | `'horizontal'` or `'vertical'` |\n| `rotation` | `number` | `90`, `180`, or `270` |\n| `outputPath` | `string` | Absolute path for the output file. Defaults to a temp file. |\n\n### `compressVideo(uri, options): Promise\u003cMediaResult\u003e`\n\nAll options are **optional**. The bitrate strategy follows this priority:\n\n1. **`targetSizeInMB`** → smart-compress: calculates optimal bitrate and resolution from duration + target size *(highest priority)*\n2. **`bitrate`** → explicit bitrate override *(takes priority over `quality`)*\n3. **`quality`** → preset mapping: `low` ≈ 1 Mbps · `medium` ≈ 4 Mbps · `high` ≈ 8 Mbps *(default)*\n\nIf none of the three are passed, the library falls back to `quality: 'medium'`.\n\n| Option | Type | Default | Description |\n|---|---|---|---|\n| `targetSizeInMB` | `number` | — | **Optional.** Target output file size in MB. When set, overrides `quality` and `bitrate`. |\n| `minResolution` | `number` | `720` | **Optional.** Minimum short-edge resolution (px) when using `targetSizeInMB`. Prevents over-downscaling. |\n| `quality` | `string` | `'medium'` | **Optional.** Preset: `'low'` \\| `'medium'` \\| `'high'`. Ignored if `targetSizeInMB` or `bitrate` is set. |\n| `bitrate` | `number` | — | **Optional.** Explicit target bitrate in bps. Overrides `quality`; ignored if `targetSizeInMB` is set. |\n| `width` | `number` | original | **Optional.** Max output width in px (aspect ratio preserved). |\n| `muteAudio` | `boolean` | `false` | **Optional.** Strip audio track from the output. |\n| `outputPath` | `string` | temp file | **Optional.** Absolute path for the output file. |\n\n### `getThumbnail(uri, options?): Promise\u003cThumbnailResult\u003e`\n\n`options` itself is optional — pass nothing to extract a full-res JPEG at time 0.\n\n| Option | Type | Default | Description |\n|---|---|---|---|\n| `timeMs` | `number` | `0` | Frame time in milliseconds |\n| `quality` | `number` | `80` | JPEG output quality (0–100) |\n| `maxWidth` | `number` | original | Max thumbnail width in px (aspect ratio preserved) |\n| `outputPath` | `string` | temp file | Absolute path for the output JPEG |\n\n### Return types\n\n```typescript\ninterface MediaResult {\n  uri: string;      // file:// URI of the output file\n  size: number;     // file size in bytes\n  width: number;    // output width in pixels\n  height: number;   // output height in pixels\n  duration: number; // duration in ms (0 for images)\n  mime: string;     // MIME type, e.g. 'video/mp4'\n}\n\ninterface ThumbnailResult {\n  uri: string;      // file:// URI of the output JPEG thumbnail\n  size: number;     // source video file size in bytes\n  width: number;    // source video width in pixels (rotation-corrected)\n  height: number;   // source video height in pixels (rotation-corrected)\n  duration: number; // source video duration in milliseconds\n}\n```\n\n\u003e **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.\n\n---\n\n## Custom UI\n\nThis library is **headless** — it provides native processing logic only, without any built-in UI.  \nYou are free to build any trim timeline, crop overlay, or progress indicator that fits your app design.\n\nThe example app (`example/src/App.tsx`) includes reference implementations of:\n- `VideoTrimBar` — a timeline scrubber with dual handles and thumbnail strip\n- `CropOverlay` — a draggable crop box with corner resize handles\n\nYou can copy these components directly into your project and adapt them as needed.\n\n\n\n---\n\n## Performance\n\nThis library is designed around two principles: avoid unnecessary work and stay on the native thread.\n\n### No bridge overhead\n\nAll 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.\n\n### Trim without re-encoding\n\n`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.\n\n### Single-pass trim + crop\n\nRunning `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.\n\n### Memory-Efficient Image Processing (OOM-Free)\n\nStandard 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**:\n- **Android:** Utilizes `BitmapFactory.Options.inSampleSize` to subsample the image during the hardware decoding phase, bypassing full-resolution memory allocation entirely.\n- **iOS:** Uses `CGImageSourceCreateThumbnailAtIndex` to instruct `ImageIO` to decode and downscale directly from the file descriptor buffer.\n\n### Smart Video Compression\n\nThe `compressVideo` API provides a dynamically balanced encoding strategy via the `targetSizeInMB` flag. When provided, the library will:\n- Calculate a bounded target `bitrate` mapped by the `duration` of the media track.\n- Adjust the output resolution dynamically, floor-bounded by `minResolution` to maintain pixel clarity at constrained bitrates.\n- Optionally strip the audio track (`muteAudio`) to allocate the entire output bandwidth to the visual presentation.\n\n### Comparison with common alternatives\n\n| Library | Native Engine | JS Bridge | Image Support | Video Support | Trim (no re-encode) | Multi-transform (1-pass) |\n|---|---|---|---|---|---|---|\n| **react-native-media-toolkit** | AVFoundation / Media3 | **JSI (Nitro)** | **Yes** (OOM-free) | **Yes** | **Yes** | **Yes** |\n| `react-native-compressor` | AVFoundation / MediaCodec | Bridge | Yes | Yes | No | No |\n| `react-native-video-trim` | AVFoundation / FFmpegKit | Bridge | No | Yes (UI included) | iOS only | No |\n\n---\n\n## Architecture\n\n```\nreact-native-media-toolkit/\n├── src/\n│   ├── MediaToolkit.nitro.ts   Nitro HybridObject spec + TypeScript types\n│   └── index.ts                Public JS/TS API\n├── ios/\n│   ├── HybridMediaToolkit.swift    Nitro entry point (Swift)\n│   ├── ImageProcessor.swift        CGImage crop and compress\n│   ├── VideoProcessor.swift        AVFoundation trim, crop, compress, thumbnail\n│   └── MediaToolkitErrors.swift    Error definitions\n├── android/\n│   └── src/main/java/com/mediatoolkit/\n│       ├── HybridMediaToolkit.kt     Nitro entry point (Kotlin)\n│       ├── ImageProcessor.kt         Bitmap crop and compress\n│       ├── VideoProcessor.kt         Media3 trim, crop, compress, thumbnail\n│       └── MediaToolkitException.kt  Error definitions\n├── nitrogen/                    Generated C++ and Swift/Kotlin bridge files\n└── example/                     Demo app (Expo Dev Client)\n```\n\n---\n\n## Contributing\n\nSee [CONTRIBUTING.md](./CONTRIBUTING.md)\n\n## License\n\nMIT — see [LICENSE](LICENSE)\n\n## Author\n\n**thangdevalone** — quangthangvtlg@gmail.com  \nGitHub: [https://github.com/thangdevalone](https://github.com/thangdevalone)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthangdevalone%2Freact-native-media-toolkit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fthangdevalone%2Freact-native-media-toolkit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthangdevalone%2Freact-native-media-toolkit/lists"}