munim-ffmpeg 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NitroMunimFfmpeg.podspec +2 -1
- package/README.md +652 -42
- package/android/build.gradle +4 -1
- package/android/src/main/java/com/margelo/nitro/munimffmpeg/HybridMunimFfmpeg.kt +2 -2
- package/app.plugin.js +89 -1
- package/ios/HybridMunimFfmpeg.swift +13 -1
- package/lib/index.d.ts +23 -0
- package/lib/index.js +73 -3
- package/package.json +32 -37
- package/scripts/patch-ffmpegkit-level.rb +14 -16
- package/src/index.ts +94 -3
package/NitroMunimFfmpeg.podspec
CHANGED
|
@@ -27,10 +27,11 @@ Pod::Spec.new do |s|
|
|
|
27
27
|
|
|
28
28
|
s.dependency 'React-jsi'
|
|
29
29
|
s.dependency 'React-callinvoker'
|
|
30
|
-
s.dependency 'ffmpeg-kit-ios-
|
|
30
|
+
s.dependency 'ffmpeg-kit-ios-full-gpl-alt', '6.0'
|
|
31
31
|
|
|
32
32
|
# ffmpeg-kit 6.0 declares negative Level values with an unsigned backing type.
|
|
33
33
|
# Xcode 26 rejects that header when Nitro enables Swift C++ interoperability.
|
|
34
|
+
# The script locates Level.h by globbing, so it survives a change of FFmpegKit pod.
|
|
34
35
|
s.script_phase = {
|
|
35
36
|
:name => 'Patch FFmpegKit Level enum for Xcode 26',
|
|
36
37
|
:script => '"${RUBY_EXECUTABLE:-/usr/bin/ruby}" "${PODS_TARGET_SRCROOT}/scripts/patch-ffmpegkit-level.rb"',
|
package/README.md
CHANGED
|
@@ -1,33 +1,274 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://github.com/munimtechnologies/munim-ffmpeg">
|
|
3
|
+
<img alt="Munim Technologies" height="128" src="https://raw.githubusercontent.com/munimtechnologies/munim-ffmpeg/main/.github/resources/banner.png?v=1" />
|
|
4
|
+
<h1 align="center">munim-ffmpeg</h1>
|
|
5
|
+
</a>
|
|
6
|
+
</p>
|
|
2
7
|
|
|
3
|
-
|
|
8
|
+
<p align="center">
|
|
9
|
+
<a aria-label="Package version" href="https://www.npmjs.com/package/munim-ffmpeg">
|
|
10
|
+
<img alt="Package version" src="https://img.shields.io/npm/v/munim-ffmpeg.svg?style=flat-square&label=Version&labelColor=000000&color=0066CC" />
|
|
11
|
+
</a>
|
|
12
|
+
<a aria-label="Package is free to use" href="https://github.com/munimtechnologies/munim-ffmpeg/blob/main/LICENSE">
|
|
13
|
+
<img alt="License: Apache-2.0" src="https://img.shields.io/badge/License-Apache%202.0-success.svg?style=flat-square&color=33CC12" />
|
|
14
|
+
</a>
|
|
15
|
+
<a aria-label="Monthly package downloads" href="https://www.npmtrends.com/munim-ffmpeg">
|
|
16
|
+
<img alt="Monthly downloads" src="https://img.shields.io/npm/dm/munim-ffmpeg.svg?style=flat-square&labelColor=gray&color=33CC12&label=Downloads" />
|
|
17
|
+
</a>
|
|
18
|
+
<a aria-label="Total package downloads" href="https://www.npmjs.com/package/munim-ffmpeg">
|
|
19
|
+
<img alt="Total downloads" src="https://img.shields.io/npm/dt/munim-ffmpeg.svg?style=flat-square&labelColor=gray&color=0066CC&label=Total%20Downloads" />
|
|
20
|
+
</a>
|
|
21
|
+
<a aria-label="Expo development builds" href="https://docs.expo.dev/develop/development-builds/introduction/">
|
|
22
|
+
<img alt="Expo development builds" src="https://img.shields.io/badge/Expo-Development%20Build-000020?style=flat-square&logo=expo&logoColor=white" />
|
|
23
|
+
</a>
|
|
24
|
+
<a aria-label="iOS 15.1 or newer" href="https://developer.apple.com/ios/">
|
|
25
|
+
<img alt="iOS 15.1+" src="https://img.shields.io/badge/iOS-15.1%2B-000000?style=flat-square&logo=apple&logoColor=white" />
|
|
26
|
+
</a>
|
|
27
|
+
<a aria-label="Android API 24 or newer" href="https://developer.android.com/">
|
|
28
|
+
<img alt="Android API 24+" src="https://img.shields.io/badge/Android-API%2024%2B-3DDC84?style=flat-square&logo=android&logoColor=white" />
|
|
29
|
+
</a>
|
|
30
|
+
<a aria-label="Powered by Nitro Modules" href="https://nitro.margelo.com/">
|
|
31
|
+
<img alt="Nitro Modules 0.36.5+" src="https://img.shields.io/badge/Nitro%20Modules-0.36.5%2B-7C3AED?style=flat-square" />
|
|
32
|
+
</a>
|
|
33
|
+
</p>
|
|
4
34
|
|
|
5
|
-
|
|
35
|
+
<p align="center">
|
|
36
|
+
<a aria-label="Works with Expo" href="https://docs.expo.dev/develop/development-builds/introduction/"><b>Works with Expo development builds</b></a>
|
|
37
|
+
 • 
|
|
38
|
+
<a aria-label="Documentation" href="https://www.munimtech.com/opensource/munim-ffmpeg">Read the Documentation</a>
|
|
39
|
+
 • 
|
|
40
|
+
<a aria-label="Report issues" href="https://github.com/munimtechnologies/munim-ffmpeg/issues">Report Issues</a>
|
|
41
|
+
</p>
|
|
6
42
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
43
|
+
<h6 align="center">Follow Munim Technologies</h6>
|
|
44
|
+
<p align="center">
|
|
45
|
+
<a aria-label="Munim Technologies on GitHub" href="https://github.com/munimtechnologies">
|
|
46
|
+
<img alt="Munim Technologies on GitHub" src="https://img.shields.io/badge/GitHub-222222?style=for-the-badge&logo=github&logoColor=white" />
|
|
47
|
+
</a>
|
|
48
|
+
<a aria-label="Munim Technologies on LinkedIn" href="https://linkedin.com/in/sheehanmunim">
|
|
49
|
+
<img alt="Munim Technologies on LinkedIn" src="https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white" />
|
|
50
|
+
</a>
|
|
51
|
+
<a aria-label="Munim Technologies website" href="https://www.munimtech.com">
|
|
52
|
+
<img alt="Munim Technologies website" src="https://img.shields.io/badge/Website-0066CC?style=for-the-badge&logo=googlechrome&logoColor=white" />
|
|
53
|
+
</a>
|
|
54
|
+
</p>
|
|
12
55
|
|
|
13
|
-
##
|
|
56
|
+
## Introduction
|
|
14
57
|
|
|
15
|
-
|
|
58
|
+
**munim-ffmpeg** provides fast, typed FFmpeg and FFprobe access for Expo and React Native. It executes argument arrays directly through native FFmpeg libraries, streams logs and encoding statistics to JavaScript, returns structured session results, inspects media with FFprobe, and cancels one running session or every active session.
|
|
59
|
+
|
|
60
|
+
**Built with React Native Nitro Modules.** One TypeScript specification generates the C++, Swift, and Kotlin bindings used by the package.
|
|
61
|
+
|
|
62
|
+
**Designed for Expo development builds and bare React Native.** This package contains native code and cannot run in Expo Go.
|
|
63
|
+
|
|
64
|
+
> **Licensing note:** The JavaScript and Nitro bridge are Apache-2.0. The bundled native FFmpeg builds are not: the Android artifact is **GPLv3** (it links x264, x265, and xvid) and the iOS artifact is **LGPLv3**. Distributing either carries obligations. Read [Bundled FFmpeg builds](#bundled-ffmpeg-builds) before shipping.
|
|
65
|
+
|
|
66
|
+
## Table of contents
|
|
67
|
+
|
|
68
|
+
- [📚 Documentation](#-documentation)
|
|
69
|
+
- [🚀 Features](#-features)
|
|
70
|
+
- [Platform support matrix](#platform-support-matrix)
|
|
71
|
+
- [Bundled FFmpeg builds](#bundled-ffmpeg-builds)
|
|
72
|
+
- [📦 Installation](#-installation)
|
|
73
|
+
- [Working with media paths](#working-with-media-paths)
|
|
74
|
+
- [⚡ Quick start](#-quick-start)
|
|
75
|
+
- [🔧 API reference](#-api-reference)
|
|
76
|
+
- [📖 Usage examples](#-usage-examples)
|
|
77
|
+
- [Licensing](#licensing)
|
|
78
|
+
- [🔍 Troubleshooting](#-troubleshooting)
|
|
79
|
+
- [Development](#development)
|
|
80
|
+
- [👏 Contributing](#-contributing)
|
|
81
|
+
- [📄 License](#-license)
|
|
82
|
+
|
|
83
|
+
## 📚 Documentation
|
|
84
|
+
|
|
85
|
+
- [Munim Technologies package documentation](https://www.munimtech.com/opensource/munim-ffmpeg)
|
|
86
|
+
- [Installation](#-installation)
|
|
87
|
+
- [API reference](#-api-reference)
|
|
88
|
+
- [Usage examples](#-usage-examples)
|
|
89
|
+
- [Troubleshooting](#-troubleshooting)
|
|
90
|
+
|
|
91
|
+
## 🚀 Features
|
|
92
|
+
|
|
93
|
+
### FFmpeg execution
|
|
94
|
+
|
|
95
|
+
- 🎬 **Argument-array commands:** Avoid platform-specific shell parsing and quoting
|
|
96
|
+
- ⚡ **Asynchronous sessions:** Keep the React Native thread responsive during native work
|
|
97
|
+
- 📝 **Live logs:** Receive FFmpeg output as it is produced
|
|
98
|
+
- 📈 **Encoding statistics:** Track time, size, bitrate, speed, frames, FPS, and quality
|
|
99
|
+
- 🎯 **Targeted cancellation:** Capture a native session ID immediately and cancel only that command
|
|
100
|
+
- 🛑 **Global cancellation:** Stop all active sessions during workflow or screen cleanup
|
|
101
|
+
|
|
102
|
+
### FFprobe and media inspection
|
|
103
|
+
|
|
104
|
+
- 🔎 **FFprobe execution:** Run custom probing commands with the same typed session result
|
|
105
|
+
- 🧾 **Parsed media information:** Inspect format, streams, chapters, codecs, duration, and metadata as JSON
|
|
106
|
+
- ✅ **Structured completion:** Read return code, state, duration, output, cancellation state, and failure details
|
|
107
|
+
|
|
108
|
+
### React Native integration
|
|
109
|
+
|
|
110
|
+
- 📱 **iOS and Android:** Native implementations in Swift and Kotlin
|
|
111
|
+
- 🧬 **Nitro Modules:** Generated high-performance native bindings
|
|
112
|
+
- 🚀 **Expo compatible:** Autolinking, config plugin, and an Expo development example
|
|
113
|
+
- 🧪 **Capability discovery:** Ask the bundled build which encoders and decoders it actually has
|
|
114
|
+
- 🎯 **TypeScript:** Complete public callback and result types
|
|
115
|
+
- 🗂️ **16 KB Android support:** Uses a maintained FFmpegKit-compatible Android artifact with 16 KB page-size support
|
|
116
|
+
|
|
117
|
+
## Platform support matrix
|
|
118
|
+
|
|
119
|
+
| Capability | iOS | Android | Notes |
|
|
120
|
+
| ---------------------------- | --------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
121
|
+
| FFmpeg argument execution | ✅ | ✅ | Commands run asynchronously through the native compatibility library. |
|
|
122
|
+
| FFprobe argument execution | ✅ | ✅ | Custom FFprobe arguments return `FFmpegSessionResult`. |
|
|
123
|
+
| Parsed media information | ✅ | ✅ | `getMediaInformation()` returns parsed FFprobe JSON. |
|
|
124
|
+
| Log callback | ✅ | ✅ | Logs are delivered while a session is active. |
|
|
125
|
+
| Encoding-statistics callback | ✅ | ✅ | Available for FFmpeg execution. |
|
|
126
|
+
| Immediate session ID | ✅ | ✅ | `onSessionCreated` fires after the native session is created. |
|
|
127
|
+
| Cancel one FFmpeg session | ✅ | ✅ | Pass the positive safe-integer ID received by `execute`'s `onSessionCreated`. The native dependency does not expose FFprobe cancellation. |
|
|
128
|
+
| Cancel all FFmpeg sessions | ✅ | ✅ | Use `cancelAll()` or call `cancel()` without an ID. |
|
|
129
|
+
| Expo Go | ❌ | ❌ | A native development build is required. |
|
|
130
|
+
| Capability discovery | ✅ | ✅ | `listEncoders()`, `listDecoders()`, and `pickEncoder()` report what the bundled build supports. |
|
|
131
|
+
| H.264 encoding | VideoToolbox | libx264 | The builds differ; use `pickEncoder(['libx264', 'h264_videotoolbox'])` instead of hard-coding an encoder. |
|
|
132
|
+
| Remote HTTP(S) inputs | ✅ | ✅ | Both builds link GnuTLS. Remote server behaviour still varies; prefer local files for predictable app workflows. |
|
|
133
|
+
|
|
134
|
+
Codec availability is determined by the native FFmpeg builds described in [Bundled FFmpeg builds](#bundled-ffmpeg-builds). Do not assume every FFmpeg codec or external library is present.
|
|
135
|
+
|
|
136
|
+
## Bundled FFmpeg builds
|
|
137
|
+
|
|
138
|
+
FFmpegKit was retired upstream in 2025 and its official binaries were withdrawn, so this package depends on maintained community rebuilds. The two platforms are **not** the same build:
|
|
139
|
+
|
|
140
|
+
| | iOS | Android |
|
|
141
|
+
| --- | --- | --- |
|
|
142
|
+
| Artifact | `ffmpeg-kit-ios-full-gpl-alt` 6.0 | `io.github.jamaismagic.ffmpeg:ffmpeg-kit-main-16kb` 6.1.7 |
|
|
143
|
+
| FFmpeg | n6.0 | n6.1.4 |
|
|
144
|
+
| Effective license | LGPLv3 | **GPLv3** |
|
|
145
|
+
| Hardware codecs | VideoToolbox, AudioToolbox | MediaCodec |
|
|
146
|
+
| 16 KB page size | n/a | ✅ (required by Google Play) |
|
|
147
|
+
|
|
148
|
+
Despite its name, the iOS pod ships FFmpeg's non-GPL configuration: it has no `libx264`, `libx265`, or `libxvid`. No public GPL build of FFmpegKit for iOS exists since the upstream retirement.
|
|
149
|
+
|
|
150
|
+
### Encoders
|
|
151
|
+
|
|
152
|
+
Verified by running the example's device suite on both platforms: iOS reports 201 encoders, Android 196, with 181 in common. Everything FFmpeg builds natively (`aac`, `alac`, `flac`, `mpeg4`, `mjpeg`, `png`, `gif`, `pcm_*`, …) is available on both, as are `libmp3lame`, `libopus`, `libvpx` (VP8), and `libvpx-vp9`.
|
|
153
|
+
|
|
154
|
+
The differences that matter:
|
|
155
|
+
|
|
156
|
+
| Encoder | iOS | Android |
|
|
157
|
+
| --- | --- | --- |
|
|
158
|
+
| `libx264` / `libx264rgb` (H.264, software) | ❌ | ✅ |
|
|
159
|
+
| `libx265` (HEVC, software) | ❌ | ✅ |
|
|
160
|
+
| `h264_videotoolbox`, `hevc_videotoolbox`, `prores_videotoolbox` | ✅ | ❌ |
|
|
161
|
+
| `h264_mediacodec`, `hevc_mediacodec`, `vp8_mediacodec`, `vp9_mediacodec`, `av1_mediacodec` | ❌ | ✅ |
|
|
162
|
+
| `libkvazaar` (HEVC, software) | ✅ | ❌ |
|
|
163
|
+
| `libtheora`, `libvorbis`, `libwebp` | ✅ | ❌ |
|
|
164
|
+
| `libspeex`, `libshine`, `libtwolame`, `libilbc`, `libopencore_amrnb`, `libvo_amrwbenc` | ✅ | ❌ |
|
|
165
|
+
| `aac_at`, `alac_at`, `ilbc_at` (AudioToolbox) | ✅ | ❌ |
|
|
166
|
+
|
|
167
|
+
iOS additionally links libass, so subtitle burn-in filters work there but not on Android.
|
|
168
|
+
|
|
169
|
+
Both platforms can encode H.264 and HEVC — just not with the same encoder name. Resolve it at runtime instead of branching on `Platform.OS`:
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
import { execute, pickEncoder } from 'munim-ffmpeg'
|
|
173
|
+
|
|
174
|
+
const h264 = await pickEncoder(['libx264', 'h264_videotoolbox'])
|
|
175
|
+
if (!h264) throw new Error('No H.264 encoder in this build')
|
|
176
|
+
|
|
177
|
+
await execute(['-y', '-i', inputPath, '-c:v', h264, outputPath])
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Decoding is far more uniform: both builds decode H.264, HEVC, VP8/VP9, AV1 (`libdav1d`), MPEG-4, MP3, AAC, Vorbis, Opus, FLAC, and the usual container formats. Both link GnuTLS, so `https://` inputs work.
|
|
181
|
+
|
|
182
|
+
The Android build is compiled with `--disable-indev=lavfi`, so `-f lavfi -i testsrc=...` and other virtual inputs are iOS-only. Feed real files or raw frames instead.
|
|
183
|
+
|
|
184
|
+
> **Avoid the `-full` and `-full-gpl` Android artifacts.** Their `libavdevice.so` references hidapi symbols that nothing in the package provides, so FFmpegKit fails to initialise at runtime with `UnsatisfiedLinkError: cannot locate symbol "PLATFORM_hid_write"`.
|
|
185
|
+
|
|
186
|
+
## 📦 Installation
|
|
187
|
+
|
|
188
|
+
### React Native CLI
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
npm install munim-ffmpeg react-native-nitro-modules
|
|
192
|
+
# or
|
|
193
|
+
yarn add munim-ffmpeg react-native-nitro-modules
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Install iOS pods after adding the dependency:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
cd ios
|
|
200
|
+
pod install
|
|
201
|
+
cd ..
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
FFmpegKit and React Native both provide `libc++_shared.so`. Resolve that duplicate in the Android application module:
|
|
205
|
+
|
|
206
|
+
```groovy
|
|
207
|
+
android {
|
|
208
|
+
packagingOptions {
|
|
209
|
+
jniLibs {
|
|
210
|
+
pickFirsts += ['**/libc++_shared.so']
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Expo
|
|
217
|
+
|
|
218
|
+
```bash
|
|
16
219
|
npx expo install munim-ffmpeg react-native-nitro-modules
|
|
17
220
|
```
|
|
18
221
|
|
|
19
|
-
|
|
222
|
+
The package includes an Expo config plugin. If your project manages its plugin list explicitly, add it to `app.json`:
|
|
223
|
+
|
|
224
|
+
```json
|
|
225
|
+
{
|
|
226
|
+
"expo": {
|
|
227
|
+
"plugins": ["munim-ffmpeg"]
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
The plugin also configures Android to select one shared C++ runtime when React Native and FFmpegKit contribute the same `libc++_shared.so` path.
|
|
233
|
+
|
|
234
|
+
Create a native development build after installation:
|
|
20
235
|
|
|
21
|
-
```
|
|
236
|
+
```bash
|
|
22
237
|
npx expo prebuild
|
|
23
238
|
npx expo run:ios
|
|
24
239
|
# or
|
|
25
240
|
npx expo run:android
|
|
26
241
|
```
|
|
27
242
|
|
|
28
|
-
|
|
243
|
+
You can also create an [EAS development build](https://docs.expo.dev/develop/development-builds/create-a-build/).
|
|
244
|
+
|
|
245
|
+
> **Important:** `munim-ffmpeg` cannot run in Expo Go because Expo Go does not include this package's native libraries.
|
|
246
|
+
|
|
247
|
+
### Requirements
|
|
248
|
+
|
|
249
|
+
- React Native with the New Architecture enabled
|
|
250
|
+
- `react-native-nitro-modules` 0.36.5 or newer
|
|
251
|
+
- iOS 15.1 or newer
|
|
252
|
+
- Android API 24 or newer
|
|
253
|
+
- An Expo development build or bare React Native app
|
|
254
|
+
|
|
255
|
+
No camera, microphone, photo-library, or storage permission is added automatically. Declare only the permissions required by the locations and capture APIs your application uses.
|
|
256
|
+
|
|
257
|
+
## Working with media paths
|
|
258
|
+
|
|
259
|
+
FFmpeg runs natively and needs a path or URI the native process can access.
|
|
260
|
+
|
|
261
|
+
- Prefer files inside your app's document, cache, or temporary directory.
|
|
262
|
+
- `file://` URIs and plain local paths both work. The package strips the `file://` scheme and percent-decoding for you, so a path containing spaces or non-ASCII characters is handled correctly; passing the raw URI straight to FFmpeg would write to a file literally named `my%20clip.mp4`.
|
|
263
|
+
- On Android, copy a `content://` document into application storage before processing when the native library cannot open it directly.
|
|
264
|
+
- Copy photo-library or document-picker assets when the provider gives temporary or security-scoped access.
|
|
265
|
+
- Ensure the output directory already exists.
|
|
266
|
+
- Use a unique output filename or pass `-y` when replacing an existing file is intentional.
|
|
267
|
+
- Do not include the `ffmpeg` or `ffprobe` executable name in the argument array.
|
|
268
|
+
|
|
269
|
+
## ⚡ Quick start
|
|
29
270
|
|
|
30
|
-
```
|
|
271
|
+
```typescript
|
|
31
272
|
import {
|
|
32
273
|
cancel,
|
|
33
274
|
execute,
|
|
@@ -36,60 +277,429 @@ import {
|
|
|
36
277
|
probe,
|
|
37
278
|
} from 'munim-ffmpeg'
|
|
38
279
|
|
|
280
|
+
console.log('FFmpeg:', getFFmpegVersion())
|
|
281
|
+
|
|
39
282
|
let activeSessionId: number | undefined
|
|
40
283
|
|
|
41
284
|
const execution = execute(
|
|
42
|
-
['-i',
|
|
285
|
+
['-y', '-i', inputPath, '-c:v', 'mpeg4', '-c:a', 'aac', outputPath],
|
|
43
286
|
(message) => console.log(message),
|
|
44
|
-
(timeMs, sizeBytes, bitrateKbits, speed) => {
|
|
45
|
-
console.log({
|
|
287
|
+
(timeMs, sizeBytes, bitrateKbits, speed, frame, fps, quality) => {
|
|
288
|
+
console.log({
|
|
289
|
+
timeMs,
|
|
290
|
+
sizeBytes,
|
|
291
|
+
bitrateKbits,
|
|
292
|
+
speed,
|
|
293
|
+
frame,
|
|
294
|
+
fps,
|
|
295
|
+
quality,
|
|
296
|
+
})
|
|
46
297
|
},
|
|
47
298
|
(sessionId) => {
|
|
48
299
|
activeSessionId = sessionId
|
|
49
300
|
}
|
|
50
301
|
)
|
|
51
302
|
|
|
52
|
-
//
|
|
53
|
-
if (activeSessionId !== undefined)
|
|
303
|
+
// Call this from a cancel button while the command is running.
|
|
304
|
+
if (activeSessionId !== undefined) {
|
|
305
|
+
cancel(activeSessionId)
|
|
306
|
+
}
|
|
54
307
|
|
|
55
308
|
const result = await execution
|
|
56
309
|
|
|
310
|
+
if (!result.success && !result.cancelled) {
|
|
311
|
+
throw new Error(result.failStackTrace ?? result.output)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const probeResult = await probe([
|
|
315
|
+
'-v',
|
|
316
|
+
'error',
|
|
317
|
+
'-show_format',
|
|
318
|
+
'-show_streams',
|
|
319
|
+
inputPath,
|
|
320
|
+
])
|
|
321
|
+
|
|
322
|
+
const mediaInformation = await getMediaInformation(inputPath)
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## 🔧 API reference
|
|
326
|
+
|
|
327
|
+
### `execute(arguments, onLog?, onStatistics?, onSessionCreated?)`
|
|
328
|
+
|
|
329
|
+
Starts an asynchronous FFmpeg session.
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
function execute(
|
|
333
|
+
arguments_: string[],
|
|
334
|
+
onLog?: (message: string) => void,
|
|
335
|
+
onStatistics?: (
|
|
336
|
+
timeMs: number,
|
|
337
|
+
sizeBytes: number,
|
|
338
|
+
bitrateKbits: number,
|
|
339
|
+
speed: number,
|
|
340
|
+
videoFrameNumber: number,
|
|
341
|
+
fps: number,
|
|
342
|
+
quality: number
|
|
343
|
+
) => void,
|
|
344
|
+
onSessionCreated?: (sessionId: number) => void
|
|
345
|
+
): Promise<FFmpegSessionResult>
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
The `onSessionCreated` callback receives the ID before the command completes, allowing targeted cancellation while work is running.
|
|
349
|
+
|
|
350
|
+
### `probe(arguments, onLog?, onSessionCreated?)`
|
|
351
|
+
|
|
352
|
+
Starts an asynchronous FFprobe session.
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
function probe(
|
|
356
|
+
arguments_: string[],
|
|
357
|
+
onLog?: (message: string) => void,
|
|
358
|
+
onSessionCreated?: (sessionId: number) => void
|
|
359
|
+
): Promise<FFmpegSessionResult>
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
`onSessionCreated` can be used to correlate the native probe session with its eventual result. The bundled native dependency does not expose FFprobe cancellation.
|
|
363
|
+
|
|
364
|
+
### `getMediaInformation(path)`
|
|
365
|
+
|
|
366
|
+
Runs FFprobe for the format, streams, and chapters at a local media path, then parses its JSON response.
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
function getMediaInformation(path: string): Promise<unknown>
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Applications should validate or narrow the returned JSON shape before using fields from it.
|
|
373
|
+
|
|
374
|
+
### `cancel(sessionId?)`
|
|
375
|
+
|
|
376
|
+
Cancels the given native FFmpeg execution session. Calling `cancel()` without an ID cancels all active FFmpeg sessions. FFprobe cancellation is not exposed by the bundled native dependency.
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
function cancel(sessionId?: number): void
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Session IDs must be positive safe integers received from `onSessionCreated` or `FFmpegSessionResult`.
|
|
383
|
+
|
|
384
|
+
### `cancelAll()`
|
|
385
|
+
|
|
386
|
+
Cancels every active FFmpeg session.
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
function cancelAll(): void
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### `getFFmpegVersion()`
|
|
393
|
+
|
|
394
|
+
Returns the version reported by the bundled native FFmpeg library.
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
function getFFmpegVersion(): string
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### `listEncoders()`
|
|
401
|
+
|
|
402
|
+
Returns the encoder names the bundled FFmpeg build can write. The result is cached after the first call.
|
|
403
|
+
|
|
404
|
+
```typescript
|
|
405
|
+
function listEncoders(): Promise<string[]>
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### `listDecoders()`
|
|
409
|
+
|
|
410
|
+
Returns the decoder names the bundled FFmpeg build can read.
|
|
411
|
+
|
|
412
|
+
```typescript
|
|
413
|
+
function listDecoders(): Promise<string[]>
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### `pickEncoder(candidates)`
|
|
417
|
+
|
|
418
|
+
Returns the first name in `candidates` that the build provides, or `undefined` when none are available. Use it to write one command that runs on both platforms.
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
function pickEncoder(candidates: string[]): Promise<string | undefined>
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
const hevc = await pickEncoder(['libx265', 'hevc_videotoolbox'])
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
### `normalizePath(value)`
|
|
429
|
+
|
|
430
|
+
Converts a `file://` URI into the plain path FFmpeg expects, and returns anything else untouched.
|
|
431
|
+
|
|
432
|
+
```typescript
|
|
433
|
+
function normalizePath(value: string): string
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
`execute()`, `probe()`, and `getMediaInformation()` already apply this to every argument, so you rarely need to call it directly. It is exported for cases where you build a path yourself — a concat list file, for example, whose entries FFmpeg reads verbatim.
|
|
437
|
+
|
|
438
|
+
### `FFmpegSessionResult`
|
|
439
|
+
|
|
440
|
+
```typescript
|
|
441
|
+
type FFmpegSessionResult = {
|
|
442
|
+
sessionId: number
|
|
443
|
+
returnCode: number
|
|
444
|
+
success: boolean
|
|
445
|
+
cancelled: boolean
|
|
446
|
+
state: string
|
|
447
|
+
durationMs: number
|
|
448
|
+
output: string
|
|
449
|
+
failStackTrace?: string
|
|
450
|
+
}
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
`state` is one of `created`, `running`, `failed`, or `completed`, and reports the same values on both platforms.
|
|
454
|
+
|
|
455
|
+
Always check `success` or `cancelled`; Promise resolution means the native session completed, not necessarily that FFmpeg returned a success code.
|
|
456
|
+
|
|
457
|
+
## 📖 Usage examples
|
|
458
|
+
|
|
459
|
+
### Inspect media metadata
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
import { getMediaInformation } from 'munim-ffmpeg'
|
|
463
|
+
|
|
464
|
+
const information = await getMediaInformation(inputPath)
|
|
465
|
+
console.log(JSON.stringify(information, null, 2))
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### Extract an audio track
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
import { execute } from 'munim-ffmpeg'
|
|
472
|
+
|
|
473
|
+
const result = await execute([
|
|
474
|
+
'-y',
|
|
475
|
+
'-i',
|
|
476
|
+
inputPath,
|
|
477
|
+
'-vn',
|
|
478
|
+
'-c:a',
|
|
479
|
+
'aac',
|
|
480
|
+
outputAudioPath,
|
|
481
|
+
])
|
|
482
|
+
|
|
57
483
|
if (!result.success) {
|
|
58
484
|
throw new Error(result.failStackTrace ?? result.output)
|
|
59
485
|
}
|
|
486
|
+
```
|
|
60
487
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
488
|
+
### Transcode to H.264 on both platforms
|
|
489
|
+
|
|
490
|
+
```typescript
|
|
491
|
+
import { execute, pickEncoder } from 'munim-ffmpeg'
|
|
492
|
+
|
|
493
|
+
const encoder = await pickEncoder(['libx264', 'h264_videotoolbox'])
|
|
494
|
+
if (!encoder) throw new Error('No H.264 encoder available in this build')
|
|
495
|
+
|
|
496
|
+
// -preset is an x264 option; VideoToolbox rejects it.
|
|
497
|
+
const quality = encoder === 'libx264' ? ['-preset', 'veryfast', '-crf', '23'] : ['-b:v', '2M']
|
|
498
|
+
|
|
499
|
+
const result = await execute([
|
|
500
|
+
'-y',
|
|
501
|
+
'-i',
|
|
502
|
+
inputPath,
|
|
503
|
+
'-c:v',
|
|
504
|
+
encoder,
|
|
505
|
+
...quality,
|
|
506
|
+
'-c:a',
|
|
507
|
+
'aac',
|
|
508
|
+
'-pix_fmt',
|
|
509
|
+
'yuv420p',
|
|
510
|
+
outputPath,
|
|
511
|
+
])
|
|
512
|
+
|
|
513
|
+
if (!result.success) throw new Error(result.failStackTrace ?? result.output)
|
|
64
514
|
```
|
|
65
515
|
|
|
66
|
-
|
|
516
|
+
### Generate a thumbnail
|
|
67
517
|
|
|
68
|
-
|
|
518
|
+
```typescript
|
|
519
|
+
import { execute } from 'munim-ffmpeg'
|
|
69
520
|
|
|
70
|
-
|
|
71
|
-
-
|
|
72
|
-
-
|
|
73
|
-
|
|
74
|
-
-
|
|
75
|
-
|
|
521
|
+
const result = await execute([
|
|
522
|
+
'-y',
|
|
523
|
+
'-ss',
|
|
524
|
+
'00:00:01.000',
|
|
525
|
+
'-i',
|
|
526
|
+
inputPath,
|
|
527
|
+
'-frames:v',
|
|
528
|
+
'1',
|
|
529
|
+
outputImagePath,
|
|
530
|
+
])
|
|
531
|
+
```
|
|
76
532
|
|
|
77
|
-
|
|
533
|
+
### Cancel a long-running command
|
|
78
534
|
|
|
79
|
-
|
|
535
|
+
```typescript
|
|
536
|
+
import { cancel, execute } from 'munim-ffmpeg'
|
|
80
537
|
|
|
81
|
-
|
|
82
|
-
- iOS: `ffmpeg-kit-ios-https-alt` 6.0
|
|
538
|
+
let sessionId: number | undefined
|
|
83
539
|
|
|
84
|
-
|
|
540
|
+
const execution = execute(
|
|
541
|
+
['-i', inputPath, '-c:v', 'mpeg4', outputPath],
|
|
542
|
+
undefined,
|
|
543
|
+
undefined,
|
|
544
|
+
(createdSessionId) => {
|
|
545
|
+
sessionId = createdSessionId
|
|
546
|
+
}
|
|
547
|
+
)
|
|
85
548
|
|
|
86
|
-
|
|
549
|
+
const cancelCurrentCommand = () => {
|
|
550
|
+
if (sessionId !== undefined) {
|
|
551
|
+
cancel(sessionId)
|
|
552
|
+
}
|
|
553
|
+
}
|
|
87
554
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
555
|
+
const result = await execution
|
|
556
|
+
console.log(result.cancelled)
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
### Run a custom FFprobe query
|
|
560
|
+
|
|
561
|
+
```typescript
|
|
562
|
+
import { probe } from 'munim-ffmpeg'
|
|
563
|
+
|
|
564
|
+
const result = await probe([
|
|
565
|
+
'-v',
|
|
566
|
+
'error',
|
|
567
|
+
'-select_streams',
|
|
568
|
+
'v:0',
|
|
569
|
+
'-show_entries',
|
|
570
|
+
'stream=codec_name,width,height,duration',
|
|
571
|
+
'-of',
|
|
572
|
+
'json',
|
|
573
|
+
inputPath,
|
|
574
|
+
])
|
|
575
|
+
|
|
576
|
+
if (result.success) {
|
|
577
|
+
console.log(result.output)
|
|
578
|
+
}
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
## Licensing
|
|
582
|
+
|
|
583
|
+
The JavaScript, TypeScript, Swift, Kotlin, and generated Nitro bridge code in this repository are Apache-2.0. The native FFmpeg binaries are not, and the two platforms differ:
|
|
584
|
+
|
|
585
|
+
| Platform | Native dependency | Version | Effective license |
|
|
586
|
+
| -------- | ----------------- | ------- | ----------------- |
|
|
587
|
+
| iOS | `ffmpeg-kit-ios-full-gpl-alt` | 6.0 | LGPLv3 (`--enable-version3`, no GPL libraries linked) |
|
|
588
|
+
| Android | `io.github.jamaismagic.ffmpeg:ffmpeg-kit-main-16kb` | 6.1.7 | **GPLv3** (`--enable-gpl` with x264 and x265) |
|
|
589
|
+
|
|
590
|
+
The Android artifact's published Maven metadata claims LGPL-3.0. That metadata is wrong: the shipped `libavcodec.so` is configured with `--enable-gpl` and the AAR bundles the x264 and x265 license notices. Treat the Android build as GPLv3.
|
|
591
|
+
|
|
592
|
+
Distributing an Android application built against it can trigger GPLv3 source, license, and redistribution obligations for your application. If that does not suit your product, replace the Android dependency in `android/build.gradle` with a non-GPL FFmpegKit artifact; H.264 encoding then relies on `h264_mediacodec`.
|
|
593
|
+
|
|
594
|
+
Before distributing an application:
|
|
595
|
+
|
|
596
|
+
1. Review the license and notices shipped by each native dependency.
|
|
597
|
+
2. Identify the codecs and linked libraries your product actually uses.
|
|
598
|
+
3. Follow the applicable LGPL, GPL, attribution, relinking, and source-offer requirements.
|
|
599
|
+
4. Reassess licensing whenever you replace a native dependency.
|
|
600
|
+
|
|
601
|
+
See [FFmpeg legal guidance](https://ffmpeg.org/legal.html). This section is an engineering reminder, not legal advice.
|
|
602
|
+
|
|
603
|
+
## 🔍 Troubleshooting
|
|
604
|
+
|
|
605
|
+
### `munim-ffmpeg` is unavailable in Expo Go
|
|
606
|
+
|
|
607
|
+
This is expected. Install the package and create an Expo development build with `npx expo run:ios`, `npx expo run:android`, or EAS Build.
|
|
608
|
+
|
|
609
|
+
### Nitro reports that `MunimFfmpeg` cannot be found
|
|
610
|
+
|
|
611
|
+
Rebuild the native app after installing both `munim-ffmpeg` and `react-native-nitro-modules`. Restarting Metro alone cannot add a native module to an existing binary.
|
|
612
|
+
|
|
613
|
+
### FFmpeg cannot open an input or output
|
|
614
|
+
|
|
615
|
+
- Confirm the file exists and the app can read it.
|
|
616
|
+
- Confirm the output directory already exists and is writable.
|
|
617
|
+
- Copy Android `content://` inputs into app storage.
|
|
618
|
+
- Copy temporary picker or photo-library assets when necessary.
|
|
619
|
+
- Log the exact argument array and native output while removing private path data from bug reports.
|
|
620
|
+
|
|
621
|
+
### A codec or filter is missing
|
|
622
|
+
|
|
623
|
+
Native FFmpeg variants do not bundle every codec, filter, or third-party library, and the iOS and Android builds are not identical. Call `listEncoders()` or `listDecoders()` to see what the running build actually has, and prefer `pickEncoder()` over a hard-coded name. `libx264` in particular exists only on Android — see [Bundled FFmpeg builds](#bundled-ffmpeg-builds).
|
|
624
|
+
|
|
625
|
+
### The Promise resolved but the command failed
|
|
626
|
+
|
|
627
|
+
Inspect `result.success`, `result.cancelled`, `result.returnCode`, `result.output`, and `result.failStackTrace`. A resolved Promise represents a completed native session; FFmpeg can still finish with a nonzero return code.
|
|
628
|
+
|
|
629
|
+
### iOS pod or build errors
|
|
630
|
+
|
|
631
|
+
Run `pod install` after installation and rebuild from a clean native development build. The package includes a narrowly scoped Xcode 26 compatibility patch for the FFmpegKit `Level` enum, which Nitro's Swift/C++ bridge otherwise rejects. The patch locates the header by globbing, so it keeps working if you swap the FFmpegKit pod.
|
|
632
|
+
|
|
633
|
+
### `building for iOS Simulator, but linking ... built for iOS`
|
|
634
|
+
|
|
635
|
+
The FFmpegKit pod tells consuming apps to exclude `arm64` from Simulator builds, which breaks Apple Silicon Macs even though its xcframework contains an `arm64` Simulator slice. The Expo config plugin removes that exclusion automatically. In a bare React Native app, add the same fix to your `Podfile`:
|
|
636
|
+
|
|
637
|
+
```ruby
|
|
638
|
+
post_install do |installer|
|
|
639
|
+
installer.pods_project.build_configurations.each do |config|
|
|
640
|
+
config.build_settings.delete('EXCLUDED_ARCHS[sdk=iphonesimulator*]')
|
|
641
|
+
end
|
|
642
|
+
installer.aggregate_targets.each do |aggregate_target|
|
|
643
|
+
aggregate_target.xcconfigs.each do |config_name, xcconfig|
|
|
644
|
+
xcconfig.attributes.delete('EXCLUDED_ARCHS[sdk=iphonesimulator*]')
|
|
645
|
+
xcconfig.save_as(Pathname.new(aggregate_target.xcconfig_path(config_name)))
|
|
646
|
+
end
|
|
647
|
+
end
|
|
648
|
+
end
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
### Android build errors
|
|
652
|
+
|
|
653
|
+
Use Android API 24 or newer, JDK 17, and the React Native New Architecture. Clear stale Gradle build output after changing native dependency versions.
|
|
654
|
+
|
|
655
|
+
If the build fails on duplicate `libc++_shared.so`, make sure the config plugin ran (Expo) or add `android.packagingOptions.pickFirsts=**/libc++_shared.so` to `gradle.properties` (bare React Native). FFmpegKit and React Native both ship that library.
|
|
656
|
+
|
|
657
|
+
## Development
|
|
658
|
+
|
|
659
|
+
```bash
|
|
660
|
+
npm install
|
|
661
|
+
npm run check # codegen, typecheck, example typecheck, build, pack dry-run
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
Individual steps are available as `npm run codegen`, `typecheck`, `typecheck:example`, and `build`.
|
|
665
|
+
|
|
666
|
+
Nitrogen output under `nitrogen/generated` is committed. Change the `.nitro.ts` specification and rerun `npm run codegen` instead of editing generated files directly.
|
|
667
|
+
|
|
668
|
+
### Example app
|
|
669
|
+
|
|
670
|
+
`example/` is an Expo app that runs a 24-check device suite: H.264 and HEVC encoding, VP9/Opus in WebM, MP3, AAC, scaling and multi-step filter graphs, muxing, demuxing, trimming, concatenation, thumbnails, audio resampling, awkward file paths, concurrent sessions, single and global cancellation, protocol support, and both failure paths. Fixtures are generated in JavaScript, so the suite needs no network or bundled media. Results are rendered on screen, written to `munim-ffmpeg-suite.json` in the app's document directory, and logged as `MUNIM_FFMPEG_SUITE_RESULT`.
|
|
671
|
+
|
|
672
|
+
```bash
|
|
673
|
+
npm run example:ios
|
|
674
|
+
# or
|
|
675
|
+
npm run example:android
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
FFmpeg encoding is slow in a simulator or emulator; run the suite on a physical device.
|
|
679
|
+
|
|
680
|
+
### Releasing
|
|
681
|
+
|
|
682
|
+
Releases run locally from a clean `main`; this repository does not use GitHub Actions.
|
|
683
|
+
|
|
684
|
+
```bash
|
|
685
|
+
npm run check
|
|
686
|
+
npm run release:local
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
`release:local` runs semantic-release with the npm token from the macOS Keychain and the GitHub CLI token, so commit messages must follow Conventional Commits.
|
|
690
|
+
|
|
691
|
+
## 👏 Contributing
|
|
692
|
+
|
|
693
|
+
Issues and pull requests are welcome. Include the following when reporting a problem:
|
|
694
|
+
|
|
695
|
+
- iOS or Android version and device architecture
|
|
696
|
+
- Expo SDK and React Native versions
|
|
697
|
+
- `react-native-nitro-modules` version
|
|
698
|
+
- Command arguments with private paths and URLs removed
|
|
699
|
+
- `FFmpegSessionResult` and relevant native logs
|
|
700
|
+
|
|
701
|
+
Please keep licensing implications explicit when proposing a new FFmpeg binary variant, codec, or linked library.
|
|
92
702
|
|
|
93
|
-
## License
|
|
703
|
+
## 📄 License
|
|
94
704
|
|
|
95
|
-
Apache
|
|
705
|
+
The `munim-ffmpeg` source is available under the [Apache License 2.0](./LICENSE). Native FFmpeg dependencies are licensed separately as described above.
|
package/android/build.gradle
CHANGED
|
@@ -140,5 +140,8 @@ dependencies {
|
|
|
140
140
|
implementation project(":react-native-nitro-modules")
|
|
141
141
|
|
|
142
142
|
// Maintained FFmpegKit-compatible Android artifact with 16 KB page support.
|
|
143
|
-
|
|
143
|
+
// Do not move to the `-full`/`-full-gpl` variants: their libavdevice.so
|
|
144
|
+
// references hidapi symbols that nothing provides, so FFmpegKit fails to load
|
|
145
|
+
// at runtime with UnsatisfiedLinkError.
|
|
146
|
+
implementation "io.github.jamaismagic.ffmpeg:ffmpeg-kit-main-16kb:6.1.7"
|
|
144
147
|
}
|
|
@@ -43,8 +43,8 @@ class HybridMunimFfmpeg : HybridMunimFfmpegSpec() {
|
|
|
43
43
|
statistics.bitrate,
|
|
44
44
|
statistics.speed,
|
|
45
45
|
statistics.videoFrameNumber.toDouble(),
|
|
46
|
-
statistics.videoFps,
|
|
47
|
-
statistics.videoQuality,
|
|
46
|
+
statistics.videoFps.toDouble(),
|
|
47
|
+
statistics.videoQuality.toDouble(),
|
|
48
48
|
)
|
|
49
49
|
},
|
|
50
50
|
)
|
package/app.plugin.js
CHANGED
|
@@ -1,5 +1,93 @@
|
|
|
1
|
+
const fs = require('node:fs')
|
|
2
|
+
const path = require('node:path')
|
|
3
|
+
const {
|
|
4
|
+
withDangerousMod,
|
|
5
|
+
withGradleProperties,
|
|
6
|
+
} = require('expo/config-plugins')
|
|
7
|
+
|
|
8
|
+
const PICK_FIRSTS_PROPERTY = 'android.packagingOptions.pickFirsts'
|
|
9
|
+
const CXX_SHARED_LIBRARY = '**/libc++_shared.so'
|
|
10
|
+
|
|
11
|
+
const PODFILE_MARKER = '# munim-ffmpeg: allow arm64 iOS Simulator builds'
|
|
12
|
+
const PODFILE_SNIPPET = ` ${PODFILE_MARKER}
|
|
13
|
+
# The FFmpegKit pod excludes arm64 from Simulator builds, which breaks Apple
|
|
14
|
+
# Silicon Macs even though its xcframework ships an arm64 Simulator slice.
|
|
15
|
+
installer.pods_project.build_configurations.each do |config|
|
|
16
|
+
config.build_settings.delete('EXCLUDED_ARCHS[sdk=iphonesimulator*]')
|
|
17
|
+
end
|
|
18
|
+
installer.aggregate_targets.each do |aggregate_target|
|
|
19
|
+
aggregate_target.xcconfigs.each do |config_name, xcconfig|
|
|
20
|
+
xcconfig.attributes.delete('EXCLUDED_ARCHS[sdk=iphonesimulator*]')
|
|
21
|
+
xcconfig.save_as(Pathname.new(aggregate_target.xcconfig_path(config_name)))
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
`
|
|
25
|
+
|
|
26
|
+
function withAndroidPackaging(config) {
|
|
27
|
+
return withGradleProperties(config, (gradleConfig) => {
|
|
28
|
+
const existing = gradleConfig.modResults.find(
|
|
29
|
+
(item) => item.type === 'property' && item.key === PICK_FIRSTS_PROPERTY
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
if (existing) {
|
|
33
|
+
const values = new Set(
|
|
34
|
+
existing.value
|
|
35
|
+
.split(',')
|
|
36
|
+
.map((value) => value.trim())
|
|
37
|
+
.filter(Boolean)
|
|
38
|
+
)
|
|
39
|
+
values.add(CXX_SHARED_LIBRARY)
|
|
40
|
+
existing.value = [...values].join(',')
|
|
41
|
+
} else {
|
|
42
|
+
gradleConfig.modResults.push({
|
|
43
|
+
type: 'property',
|
|
44
|
+
key: PICK_FIRSTS_PROPERTY,
|
|
45
|
+
value: CXX_SHARED_LIBRARY,
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return gradleConfig
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function withSimulatorArchitectures(config) {
|
|
54
|
+
return withDangerousMod(config, [
|
|
55
|
+
'ios',
|
|
56
|
+
(modConfig) => {
|
|
57
|
+
const podfilePath = path.join(
|
|
58
|
+
modConfig.modRequest.platformProjectRoot,
|
|
59
|
+
'Podfile'
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if (!fs.existsSync(podfilePath)) return modConfig
|
|
63
|
+
|
|
64
|
+
const contents = fs.readFileSync(podfilePath, 'utf8')
|
|
65
|
+
if (contents.includes(PODFILE_MARKER)) return modConfig
|
|
66
|
+
|
|
67
|
+
const postInstall = /^([ \t]*)post_install do \|(\w+)\|[ \t]*$/m
|
|
68
|
+
const match = contents.match(postInstall)
|
|
69
|
+
|
|
70
|
+
if (!match) {
|
|
71
|
+
console.warn(
|
|
72
|
+
'munim-ffmpeg: no post_install block found in the Podfile; arm64 Simulator builds may fail.'
|
|
73
|
+
)
|
|
74
|
+
return modConfig
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const snippet = PODFILE_SNIPPET.replace(/\binstaller\b/g, match[2])
|
|
78
|
+
const patched = contents.replace(
|
|
79
|
+
postInstall,
|
|
80
|
+
(line) => `${line}\n${snippet}`
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
fs.writeFileSync(podfilePath, patched)
|
|
84
|
+
return modConfig
|
|
85
|
+
},
|
|
86
|
+
])
|
|
87
|
+
}
|
|
88
|
+
|
|
1
89
|
module.exports = function withMunimFfmpeg(config) {
|
|
2
|
-
return config
|
|
90
|
+
return withSimulatorArchitectures(withAndroidPackaging(config))
|
|
3
91
|
}
|
|
4
92
|
|
|
5
93
|
module.exports.default = module.exports
|
|
@@ -128,6 +128,18 @@ final class HybridMunimFfmpeg: HybridMunimFfmpegSpec {
|
|
|
128
128
|
FFmpegKit.cancel()
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
// `String(describing:)` on the imported NS_ENUM yields "sessionstate(rawvalue: 3)".
|
|
132
|
+
// Android reports the Java enum name, so map to the same lowercase names here.
|
|
133
|
+
private static func stateName(_ state: SessionState) -> String {
|
|
134
|
+
switch state {
|
|
135
|
+
case .created: return "created"
|
|
136
|
+
case .running: return "running"
|
|
137
|
+
case .failed: return "failed"
|
|
138
|
+
case .completed: return "completed"
|
|
139
|
+
@unknown default: return "unknown"
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
131
143
|
private static func result(from session: Session) -> FFmpegSessionResult {
|
|
132
144
|
let returnCode = session.getReturnCode()
|
|
133
145
|
return FFmpegSessionResult(
|
|
@@ -135,7 +147,7 @@ final class HybridMunimFfmpeg: HybridMunimFfmpegSpec {
|
|
|
135
147
|
returnCode: Double(returnCode?.getValue() ?? -1),
|
|
136
148
|
success: ReturnCode.isSuccess(returnCode),
|
|
137
149
|
cancelled: ReturnCode.isCancel(returnCode),
|
|
138
|
-
state:
|
|
150
|
+
state: Self.stateName(session.getState()),
|
|
139
151
|
durationMs: Double(session.getDuration()),
|
|
140
152
|
output: session.getOutput() ?? "",
|
|
141
153
|
failStackTrace: session.getFailStackTrace()
|
package/lib/index.d.ts
CHANGED
|
@@ -1,10 +1,33 @@
|
|
|
1
1
|
import type { FFmpegLogCallback, FFmpegSessionResult, FFmpegSessionCreatedCallback, FFmpegStatisticsCallback, MunimFfmpeg as MunimFfmpegSpec } from './specs/MunimFfmpeg.nitro';
|
|
2
2
|
declare const MunimFfmpeg: MunimFfmpegSpec;
|
|
3
3
|
export type { FFmpegLogCallback, FFmpegSessionResult, FFmpegSessionCreatedCallback, FFmpegStatisticsCallback, MunimFfmpegSpec, };
|
|
4
|
+
/**
|
|
5
|
+
* Converts a `file://` URI into the plain path FFmpeg expects.
|
|
6
|
+
*
|
|
7
|
+
* `expo-file-system` and `react-native-fs` hand back percent-encoded URIs, but
|
|
8
|
+
* FFmpeg's file protocol treats what follows `file://` literally: a path with a
|
|
9
|
+
* space silently becomes a file named `my%20clip.mp4`. Anything that is not a
|
|
10
|
+
* `file://` URI is returned untouched, so pipes, `content://`, and remote URLs
|
|
11
|
+
* still work.
|
|
12
|
+
*/
|
|
13
|
+
export declare function normalizePath(value: string): string;
|
|
4
14
|
export declare function execute(arguments_: string[], onLog?: FFmpegLogCallback, onStatistics?: FFmpegStatisticsCallback, onSessionCreated?: FFmpegSessionCreatedCallback): Promise<FFmpegSessionResult>;
|
|
5
15
|
export declare function probe(arguments_: string[], onLog?: FFmpegLogCallback, onSessionCreated?: FFmpegSessionCreatedCallback): Promise<FFmpegSessionResult>;
|
|
6
16
|
export declare function getMediaInformation(path: string): Promise<unknown>;
|
|
7
17
|
export declare function cancel(sessionId?: number): void;
|
|
8
18
|
export declare function cancelAll(): void;
|
|
9
19
|
export declare function getFFmpegVersion(): string;
|
|
20
|
+
/** Encoder names the bundled FFmpeg build can write, e.g. `libx264`. */
|
|
21
|
+
export declare function listEncoders(): Promise<string[]>;
|
|
22
|
+
/** Decoder names the bundled FFmpeg build can read, e.g. `h264`. */
|
|
23
|
+
export declare function listDecoders(): Promise<string[]>;
|
|
24
|
+
/**
|
|
25
|
+
* Returns the first available encoder from `candidates`, so one command can
|
|
26
|
+
* run on both platforms:
|
|
27
|
+
*
|
|
28
|
+
* ```ts
|
|
29
|
+
* const encoder = await pickEncoder(['libx264', 'h264_videotoolbox'])
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export declare function pickEncoder(candidates: string[]): Promise<string | undefined>;
|
|
10
33
|
export default MunimFfmpeg;
|
package/lib/index.js
CHANGED
|
@@ -1,13 +1,34 @@
|
|
|
1
1
|
import { NitroModules } from 'react-native-nitro-modules';
|
|
2
2
|
const MunimFfmpeg = NitroModules.createHybridObject('MunimFfmpeg');
|
|
3
|
+
const FILE_URI_SCHEME = /^file:\/\//;
|
|
4
|
+
/**
|
|
5
|
+
* Converts a `file://` URI into the plain path FFmpeg expects.
|
|
6
|
+
*
|
|
7
|
+
* `expo-file-system` and `react-native-fs` hand back percent-encoded URIs, but
|
|
8
|
+
* FFmpeg's file protocol treats what follows `file://` literally: a path with a
|
|
9
|
+
* space silently becomes a file named `my%20clip.mp4`. Anything that is not a
|
|
10
|
+
* `file://` URI is returned untouched, so pipes, `content://`, and remote URLs
|
|
11
|
+
* still work.
|
|
12
|
+
*/
|
|
13
|
+
export function normalizePath(value) {
|
|
14
|
+
if (!FILE_URI_SCHEME.test(value))
|
|
15
|
+
return value;
|
|
16
|
+
const path = value.replace(FILE_URI_SCHEME, '');
|
|
17
|
+
try {
|
|
18
|
+
return decodeURIComponent(path);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return path;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
3
24
|
export function execute(arguments_, onLog, onStatistics, onSessionCreated) {
|
|
4
|
-
return MunimFfmpeg.execute(arguments_, onLog, onStatistics, onSessionCreated);
|
|
25
|
+
return MunimFfmpeg.execute(arguments_.map(normalizePath), onLog, onStatistics, onSessionCreated);
|
|
5
26
|
}
|
|
6
27
|
export function probe(arguments_, onLog, onSessionCreated) {
|
|
7
|
-
return MunimFfmpeg.probe(arguments_, onLog, onSessionCreated);
|
|
28
|
+
return MunimFfmpeg.probe(arguments_.map(normalizePath), onLog, onSessionCreated);
|
|
8
29
|
}
|
|
9
30
|
export function getMediaInformation(path) {
|
|
10
|
-
return MunimFfmpeg.getMediaInformation(path).then((value) => JSON.parse(value));
|
|
31
|
+
return MunimFfmpeg.getMediaInformation(normalizePath(path)).then((value) => JSON.parse(value));
|
|
11
32
|
}
|
|
12
33
|
export function cancel(sessionId) {
|
|
13
34
|
MunimFfmpeg.cancel(sessionId);
|
|
@@ -18,4 +39,53 @@ export function cancelAll() {
|
|
|
18
39
|
export function getFFmpegVersion() {
|
|
19
40
|
return MunimFfmpeg.ffmpegVersion;
|
|
20
41
|
}
|
|
42
|
+
// The bundled FFmpeg builds differ per platform: Android ships libx264/libx265,
|
|
43
|
+
// iOS ships the VideoToolbox hardware encoders instead. Asking the binary what
|
|
44
|
+
// it supports is more reliable than hard-coding a per-platform table.
|
|
45
|
+
const codecCache = new Map();
|
|
46
|
+
function listCodecs(flag) {
|
|
47
|
+
const cached = codecCache.get(flag);
|
|
48
|
+
if (cached)
|
|
49
|
+
return cached;
|
|
50
|
+
const request = execute(['-hide_banner', flag])
|
|
51
|
+
.then((result) => {
|
|
52
|
+
if (!result.success) {
|
|
53
|
+
throw new Error(result.failStackTrace ?? result.output);
|
|
54
|
+
}
|
|
55
|
+
// Each entry is printed as `<capability flags> <name> <description>`
|
|
56
|
+
// below a line of dashes.
|
|
57
|
+
const body = result.output.split(/^\s*-+\s*$/m).pop() ?? '';
|
|
58
|
+
return body
|
|
59
|
+
.split('\n')
|
|
60
|
+
.map((line) => line.trim().split(/\s+/))
|
|
61
|
+
.filter((columns) => columns.length >= 2 && /^[A-Z.]{6}$/.test(columns[0]))
|
|
62
|
+
.map((columns) => columns[1]);
|
|
63
|
+
})
|
|
64
|
+
.catch((error) => {
|
|
65
|
+
codecCache.delete(flag);
|
|
66
|
+
throw error;
|
|
67
|
+
});
|
|
68
|
+
codecCache.set(flag, request);
|
|
69
|
+
return request;
|
|
70
|
+
}
|
|
71
|
+
/** Encoder names the bundled FFmpeg build can write, e.g. `libx264`. */
|
|
72
|
+
export function listEncoders() {
|
|
73
|
+
return listCodecs('-encoders');
|
|
74
|
+
}
|
|
75
|
+
/** Decoder names the bundled FFmpeg build can read, e.g. `h264`. */
|
|
76
|
+
export function listDecoders() {
|
|
77
|
+
return listCodecs('-decoders');
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Returns the first available encoder from `candidates`, so one command can
|
|
81
|
+
* run on both platforms:
|
|
82
|
+
*
|
|
83
|
+
* ```ts
|
|
84
|
+
* const encoder = await pickEncoder(['libx264', 'h264_videotoolbox'])
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export async function pickEncoder(candidates) {
|
|
88
|
+
const encoders = new Set(await listEncoders());
|
|
89
|
+
return candidates.find((candidate) => encoders.has(candidate));
|
|
90
|
+
}
|
|
21
91
|
export default MunimFfmpeg;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "munim-ffmpeg",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Fast FFmpeg and FFprobe for Expo and React Native, powered by Nitro Modules",
|
|
5
5
|
"main": "lib/index",
|
|
6
6
|
"module": "lib/index",
|
|
@@ -23,23 +23,28 @@
|
|
|
23
23
|
"ios/**/*.mm",
|
|
24
24
|
"ios/**/*.cpp",
|
|
25
25
|
"ios/**/*.swift",
|
|
26
|
-
"scripts",
|
|
27
26
|
"app.plugin.js",
|
|
28
27
|
"nitro.json",
|
|
29
28
|
"*.podspec",
|
|
30
29
|
"README.md",
|
|
31
|
-
"LICENSE"
|
|
30
|
+
"LICENSE",
|
|
31
|
+
"scripts/patch-ffmpegkit-level.rb"
|
|
32
32
|
],
|
|
33
33
|
"scripts": {
|
|
34
34
|
"typecheck": "tsc --noEmit",
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"lint
|
|
35
|
+
"typecheck:example": "tsc --noEmit -p example/tsconfig.json",
|
|
36
|
+
"clean": "rm -rf lib android/build example/ios example/android",
|
|
37
|
+
"lint": "eslint .",
|
|
38
|
+
"codegen": "nitrogen --logLevel=\"debug\"",
|
|
39
|
+
"specs": "npm run codegen",
|
|
38
40
|
"build": "npm run typecheck && tsc",
|
|
39
41
|
"prepack": "npm run build",
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
42
|
+
"check": "npm run codegen && npm run typecheck && npm run typecheck:example && npm run build && npm pack --dry-run",
|
|
43
|
+
"example:start": "npm --workspace example run start",
|
|
44
|
+
"example:ios": "npm --workspace example run ios",
|
|
45
|
+
"example:android": "npm --workspace example run android",
|
|
46
|
+
"release:local": "node scripts/release-local.mjs",
|
|
47
|
+
"format": "prettier --write \"**/*.{ts,tsx,js,cjs,mjs,json,md}\""
|
|
43
48
|
},
|
|
44
49
|
"keywords": [
|
|
45
50
|
"react-native",
|
|
@@ -53,8 +58,17 @@
|
|
|
53
58
|
"typescript",
|
|
54
59
|
"nitro",
|
|
55
60
|
"nitro-modules",
|
|
61
|
+
"react-native-ffmpeg",
|
|
62
|
+
"expo-plugin",
|
|
63
|
+
"video-processing",
|
|
64
|
+
"audio-processing",
|
|
65
|
+
"media-processing",
|
|
66
|
+
"transcoding",
|
|
56
67
|
"munim-technologies"
|
|
57
68
|
],
|
|
69
|
+
"workspaces": [
|
|
70
|
+
"example"
|
|
71
|
+
],
|
|
58
72
|
"repository": {
|
|
59
73
|
"type": "git",
|
|
60
74
|
"url": "git+https://github.com/munimtechnologies/munim-ffmpeg.git"
|
|
@@ -77,49 +91,30 @@
|
|
|
77
91
|
]
|
|
78
92
|
},
|
|
79
93
|
"devDependencies": {
|
|
80
|
-
"@
|
|
94
|
+
"@eslint/eslintrc": "^3.3.6",
|
|
95
|
+
"@eslint/js": "^9.39.0",
|
|
96
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
97
|
+
"@semantic-release/git": "^10.0.1",
|
|
81
98
|
"@types/react": "^19.2.15",
|
|
99
|
+
"conventional-changelog-conventionalcommits": "^9.3.1",
|
|
82
100
|
"eslint": "^9.39.4",
|
|
83
101
|
"eslint-config-prettier": "^10.1.8",
|
|
84
102
|
"eslint-plugin-prettier": "^5.5.5",
|
|
103
|
+
"globals": "^17.11.0",
|
|
85
104
|
"nitrogen": "0.36.5",
|
|
86
105
|
"prettier": "^3.8.3",
|
|
87
106
|
"react": "19.2.3",
|
|
88
107
|
"react-native": "0.86.2",
|
|
89
108
|
"react-native-nitro-modules": "^0.36.5",
|
|
90
|
-
"
|
|
109
|
+
"semantic-release": "^25.0.9",
|
|
110
|
+
"typescript": "^6.0.3",
|
|
111
|
+
"typescript-eslint": "^8.46.0"
|
|
91
112
|
},
|
|
92
113
|
"peerDependencies": {
|
|
93
114
|
"react": "*",
|
|
94
115
|
"react-native": "*",
|
|
95
116
|
"react-native-nitro-modules": ">=0.36.5 <1"
|
|
96
117
|
},
|
|
97
|
-
"eslintConfig": {
|
|
98
|
-
"root": true,
|
|
99
|
-
"extends": [
|
|
100
|
-
"@react-native",
|
|
101
|
-
"prettier"
|
|
102
|
-
],
|
|
103
|
-
"plugins": [
|
|
104
|
-
"prettier"
|
|
105
|
-
],
|
|
106
|
-
"rules": {
|
|
107
|
-
"prettier/prettier": [
|
|
108
|
-
"warn",
|
|
109
|
-
{
|
|
110
|
-
"quoteProps": "consistent",
|
|
111
|
-
"singleQuote": true,
|
|
112
|
-
"tabWidth": 2,
|
|
113
|
-
"trailingComma": "es5",
|
|
114
|
-
"useTabs": false
|
|
115
|
-
}
|
|
116
|
-
]
|
|
117
|
-
}
|
|
118
|
-
},
|
|
119
|
-
"eslintIgnore": [
|
|
120
|
-
"node_modules/",
|
|
121
|
-
"lib/"
|
|
122
|
-
],
|
|
123
118
|
"prettier": {
|
|
124
119
|
"quoteProps": "consistent",
|
|
125
120
|
"singleQuote": true,
|
|
@@ -3,24 +3,22 @@
|
|
|
3
3
|
require 'fileutils'
|
|
4
4
|
require 'tempfile'
|
|
5
5
|
|
|
6
|
+
# FFmpegKit 6.0 declares `Level` with an unsigned backing type while assigning
|
|
7
|
+
# negative values to it. Clang accepts that in Objective-C, but Xcode 26 rejects
|
|
8
|
+
# the header once Nitro turns on Swift/C++ interoperability, so every vendored
|
|
9
|
+
# and copied copy of Level.h is rewritten to a signed enum before compiling.
|
|
10
|
+
#
|
|
11
|
+
# The header is located by globbing rather than by pod name: the FFmpegKit
|
|
12
|
+
# republishes lay their xcframeworks out differently (`<pod>/ffmpegkit.xcframework`
|
|
13
|
+
# versus `<pod>/xcframeworks/ffmpegkit.xcframework`), and the pod itself may be
|
|
14
|
+
# swapped for another variant.
|
|
6
15
|
pods_root = ENV.fetch('PODS_ROOT')
|
|
7
16
|
build_products = ENV.fetch('PODS_CONFIGURATION_BUILD_DIR')
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
File.join(source_root, 'ios-arm64', 'ffmpegkit.framework', 'Headers', 'Level.h'),
|
|
14
|
-
File.join(source_root, 'ios-arm64_x86_64-simulator', 'ffmpegkit.framework', 'Headers', 'Level.h'),
|
|
15
|
-
File.join(
|
|
16
|
-
build_products,
|
|
17
|
-
'XCFrameworkIntermediates',
|
|
18
|
-
'ffmpeg-kit-ios-https-alt',
|
|
19
|
-
'ffmpegkit.framework',
|
|
20
|
-
'Headers',
|
|
21
|
-
'Level.h'
|
|
22
|
-
),
|
|
23
|
-
].select { |path| File.file?(path) }
|
|
17
|
+
|
|
18
|
+
headers = [pods_root, build_products]
|
|
19
|
+
.flat_map { |root| Dir.glob(File.join(root, '**', 'ffmpegkit.framework', 'Headers', 'Level.h')) }
|
|
20
|
+
.uniq
|
|
21
|
+
.select { |path| File.file?(path) }
|
|
24
22
|
|
|
25
23
|
abort('munim-ffmpeg: could not find the expected FFmpegKit Level.h') if headers.empty?
|
|
26
24
|
|
package/src/index.ts
CHANGED
|
@@ -18,13 +18,40 @@ export type {
|
|
|
18
18
|
MunimFfmpegSpec,
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
const FILE_URI_SCHEME = /^file:\/\//
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Converts a `file://` URI into the plain path FFmpeg expects.
|
|
25
|
+
*
|
|
26
|
+
* `expo-file-system` and `react-native-fs` hand back percent-encoded URIs, but
|
|
27
|
+
* FFmpeg's file protocol treats what follows `file://` literally: a path with a
|
|
28
|
+
* space silently becomes a file named `my%20clip.mp4`. Anything that is not a
|
|
29
|
+
* `file://` URI is returned untouched, so pipes, `content://`, and remote URLs
|
|
30
|
+
* still work.
|
|
31
|
+
*/
|
|
32
|
+
export function normalizePath(value: string): string {
|
|
33
|
+
if (!FILE_URI_SCHEME.test(value)) return value
|
|
34
|
+
|
|
35
|
+
const path = value.replace(FILE_URI_SCHEME, '')
|
|
36
|
+
try {
|
|
37
|
+
return decodeURIComponent(path)
|
|
38
|
+
} catch {
|
|
39
|
+
return path
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
21
43
|
export function execute(
|
|
22
44
|
arguments_: string[],
|
|
23
45
|
onLog?: FFmpegLogCallback,
|
|
24
46
|
onStatistics?: FFmpegStatisticsCallback,
|
|
25
47
|
onSessionCreated?: FFmpegSessionCreatedCallback
|
|
26
48
|
): Promise<FFmpegSessionResult> {
|
|
27
|
-
return MunimFfmpeg.execute(
|
|
49
|
+
return MunimFfmpeg.execute(
|
|
50
|
+
arguments_.map(normalizePath),
|
|
51
|
+
onLog,
|
|
52
|
+
onStatistics,
|
|
53
|
+
onSessionCreated
|
|
54
|
+
)
|
|
28
55
|
}
|
|
29
56
|
|
|
30
57
|
export function probe(
|
|
@@ -32,11 +59,15 @@ export function probe(
|
|
|
32
59
|
onLog?: FFmpegLogCallback,
|
|
33
60
|
onSessionCreated?: FFmpegSessionCreatedCallback
|
|
34
61
|
): Promise<FFmpegSessionResult> {
|
|
35
|
-
return MunimFfmpeg.probe(
|
|
62
|
+
return MunimFfmpeg.probe(
|
|
63
|
+
arguments_.map(normalizePath),
|
|
64
|
+
onLog,
|
|
65
|
+
onSessionCreated
|
|
66
|
+
)
|
|
36
67
|
}
|
|
37
68
|
|
|
38
69
|
export function getMediaInformation(path: string): Promise<unknown> {
|
|
39
|
-
return MunimFfmpeg.getMediaInformation(path).then((value) =>
|
|
70
|
+
return MunimFfmpeg.getMediaInformation(normalizePath(path)).then((value) =>
|
|
40
71
|
JSON.parse(value)
|
|
41
72
|
)
|
|
42
73
|
}
|
|
@@ -53,4 +84,64 @@ export function getFFmpegVersion(): string {
|
|
|
53
84
|
return MunimFfmpeg.ffmpegVersion
|
|
54
85
|
}
|
|
55
86
|
|
|
87
|
+
// The bundled FFmpeg builds differ per platform: Android ships libx264/libx265,
|
|
88
|
+
// iOS ships the VideoToolbox hardware encoders instead. Asking the binary what
|
|
89
|
+
// it supports is more reliable than hard-coding a per-platform table.
|
|
90
|
+
const codecCache = new Map<string, Promise<string[]>>()
|
|
91
|
+
|
|
92
|
+
function listCodecs(flag: '-encoders' | '-decoders'): Promise<string[]> {
|
|
93
|
+
const cached = codecCache.get(flag)
|
|
94
|
+
if (cached) return cached
|
|
95
|
+
|
|
96
|
+
const request = execute(['-hide_banner', flag])
|
|
97
|
+
.then((result) => {
|
|
98
|
+
if (!result.success) {
|
|
99
|
+
throw new Error(result.failStackTrace ?? result.output)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Each entry is printed as `<capability flags> <name> <description>`
|
|
103
|
+
// below a line of dashes.
|
|
104
|
+
const body = result.output.split(/^\s*-+\s*$/m).pop() ?? ''
|
|
105
|
+
return body
|
|
106
|
+
.split('\n')
|
|
107
|
+
.map((line) => line.trim().split(/\s+/))
|
|
108
|
+
.filter(
|
|
109
|
+
(columns) => columns.length >= 2 && /^[A-Z.]{6}$/.test(columns[0]!)
|
|
110
|
+
)
|
|
111
|
+
.map((columns) => columns[1]!)
|
|
112
|
+
})
|
|
113
|
+
.catch((error) => {
|
|
114
|
+
codecCache.delete(flag)
|
|
115
|
+
throw error
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
codecCache.set(flag, request)
|
|
119
|
+
return request
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Encoder names the bundled FFmpeg build can write, e.g. `libx264`. */
|
|
123
|
+
export function listEncoders(): Promise<string[]> {
|
|
124
|
+
return listCodecs('-encoders')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Decoder names the bundled FFmpeg build can read, e.g. `h264`. */
|
|
128
|
+
export function listDecoders(): Promise<string[]> {
|
|
129
|
+
return listCodecs('-decoders')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Returns the first available encoder from `candidates`, so one command can
|
|
134
|
+
* run on both platforms:
|
|
135
|
+
*
|
|
136
|
+
* ```ts
|
|
137
|
+
* const encoder = await pickEncoder(['libx264', 'h264_videotoolbox'])
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
export async function pickEncoder(
|
|
141
|
+
candidates: string[]
|
|
142
|
+
): Promise<string | undefined> {
|
|
143
|
+
const encoders = new Set(await listEncoders())
|
|
144
|
+
return candidates.find((candidate) => encoders.has(candidate))
|
|
145
|
+
}
|
|
146
|
+
|
|
56
147
|
export default MunimFfmpeg
|