munim-ffmpeg 0.2.0 β 0.3.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 +15 -10
- package/README.md +60 -64
- package/android/build.gradle +2 -5
- package/android/src/main/java/com/margelo/nitro/munimffmpeg/FFmpegNative.kt +86 -0
- package/android/src/main/java/com/margelo/nitro/munimffmpeg/HybridMunimFfmpeg.kt +100 -63
- package/ios/Bridge.h +4 -1
- package/ios/HybridMunimFfmpeg.swift +177 -103
- package/ios/munim_ffmpeg_core.h +58 -0
- package/package.json +13 -4
- package/scripts/binaries.json +5 -0
- package/scripts/fetch-binaries.mjs +115 -0
- package/scripts/patch-ffmpegkit-level.rb +0 -43
package/NitroMunimFfmpeg.podspec
CHANGED
|
@@ -18,25 +18,30 @@ Pod::Spec.new do |s|
|
|
|
18
18
|
"ios/**/*.{swift}",
|
|
19
19
|
# Autolinking/Registration (Objective-C++)
|
|
20
20
|
"ios/**/*.{m,mm}",
|
|
21
|
+
# C core that drives FFmpeg's in-process command-line tools
|
|
22
|
+
"ios/*.h",
|
|
21
23
|
# Implementation (C++ objects)
|
|
22
24
|
"cpp/**/*.{hpp,cpp}",
|
|
23
25
|
]
|
|
24
26
|
|
|
27
|
+
# FFmpeg 9 and the in-process fftools core. Downloaded by
|
|
28
|
+
# scripts/fetch-binaries.mjs on install, or built by scripts/ffmpeg/build-all.sh.
|
|
29
|
+
s.vendored_frameworks = "ios/MunimFFmpeg.xcframework"
|
|
30
|
+
s.pod_target_xcconfig = {
|
|
31
|
+
"HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/ios\"",
|
|
32
|
+
}
|
|
33
|
+
s.frameworks = "AudioToolbox", "VideoToolbox", "CoreMedia", "AVFoundation", "CoreVideo", "Security"
|
|
34
|
+
s.libraries = "bz2", "z", "iconv", "c++"
|
|
35
|
+
|
|
36
|
+
# Must be public so CocoaPods puts it in the module umbrella, which is how the
|
|
37
|
+
# Swift implementation sees the C core.
|
|
38
|
+
s.public_header_files = "ios/munim_ffmpeg_core.h"
|
|
39
|
+
|
|
25
40
|
load 'nitrogen/generated/ios/NitroMunimFfmpeg+autolinking.rb'
|
|
26
41
|
add_nitrogen_files(s)
|
|
27
42
|
|
|
28
43
|
s.dependency 'React-jsi'
|
|
29
44
|
s.dependency 'React-callinvoker'
|
|
30
|
-
s.dependency 'ffmpeg-kit-ios-full-gpl-alt', '6.0'
|
|
31
|
-
|
|
32
|
-
# ffmpeg-kit 6.0 declares negative Level values with an unsigned backing type.
|
|
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.
|
|
35
|
-
s.script_phase = {
|
|
36
|
-
:name => 'Patch FFmpegKit Level enum for Xcode 26',
|
|
37
|
-
:script => '"${RUBY_EXECUTABLE:-/usr/bin/ruby}" "${PODS_TARGET_SRCROOT}/scripts/patch-ffmpegkit-level.rb"',
|
|
38
|
-
:execution_position => :before_compile,
|
|
39
|
-
}
|
|
40
45
|
|
|
41
46
|
install_modules_dependencies(s)
|
|
42
47
|
end
|
package/README.md
CHANGED
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
|
|
62
62
|
**Designed for Expo development builds and bare React Native.** This package contains native code and cannot run in Expo Go.
|
|
63
63
|
|
|
64
|
-
> **Licensing note:** The JavaScript and Nitro bridge are Apache-2.0. The bundled
|
|
64
|
+
> **Licensing note:** The JavaScript, Swift, Kotlin, and Nitro bridge are Apache-2.0. The bundled FFmpeg is **LGPLv3** on both platforms β it deliberately excludes x264, x265, and xvid, so your app does not inherit GPL obligations. See [Licensing](#licensing).
|
|
65
65
|
|
|
66
66
|
## Table of contents
|
|
67
67
|
|
|
@@ -92,7 +92,8 @@
|
|
|
92
92
|
|
|
93
93
|
### FFmpeg execution
|
|
94
94
|
|
|
95
|
-
- π¬ **Argument-array commands:**
|
|
95
|
+
- π¬ **Argument-array commands:** FFmpeg's own CLI code paths, without shell parsing or quoting
|
|
96
|
+
- π **FFmpeg 9.0.1:** The current upstream release, identical on both platforms
|
|
96
97
|
- β‘ **Asynchronous sessions:** Keep the React Native thread responsive during native work
|
|
97
98
|
- π **Live logs:** Receive FFmpeg output as it is produced
|
|
98
99
|
- π **Encoding statistics:** Track time, size, bitrate, speed, frames, FPS, and quality
|
|
@@ -112,7 +113,7 @@
|
|
|
112
113
|
- π **Expo compatible:** Autolinking, config plugin, and an Expo development example
|
|
113
114
|
- π§ͺ **Capability discovery:** Ask the bundled build which encoders and decoders it actually has
|
|
114
115
|
- π― **TypeScript:** Complete public callback and result types
|
|
115
|
-
- ποΈ **16 KB Android
|
|
116
|
+
- ποΈ **16 KB Android pages:** Built with the alignment Google Play requires
|
|
116
117
|
|
|
117
118
|
## Platform support matrix
|
|
118
119
|
|
|
@@ -135,53 +136,46 @@ Codec availability is determined by the native FFmpeg builds described in [Bundl
|
|
|
135
136
|
|
|
136
137
|
## Bundled FFmpeg builds
|
|
137
138
|
|
|
138
|
-
|
|
139
|
+
Both platforms run **FFmpeg 9.0.1**, built from [ffmpeg.org](https://www.ffmpeg.org/) by the scripts in [`scripts/ffmpeg/`](./scripts/ffmpeg). There is no FFmpegKit here: that project was retired in 2025 and pinned to FFmpeg 6.0.
|
|
139
140
|
|
|
140
141
|
| | iOS | Android |
|
|
141
142
|
| --- | --- | --- |
|
|
142
|
-
|
|
|
143
|
-
|
|
|
144
|
-
| Effective license | LGPLv3 | **GPLv3** |
|
|
143
|
+
| FFmpeg | 9.0.1 | 9.0.1 |
|
|
144
|
+
| Architectures | arm64 device, arm64 + x86_64 simulator | arm64-v8a, armeabi-v7a, x86_64 |
|
|
145
145
|
| Hardware codecs | VideoToolbox, AudioToolbox | MediaCodec |
|
|
146
|
-
|
|
|
146
|
+
| TLS | SecureTransport | mbedTLS |
|
|
147
|
+
| Minimum | iOS 15.1 | API 24, 16 KB pages |
|
|
147
148
|
|
|
148
|
-
|
|
149
|
+
Linked libraries, identical on both: **LAME** (MP3), **Opus**, **libvpx** (VP8/VP9), **dav1d** (AV1 decoding), plus everything FFmpeg builds natively.
|
|
150
|
+
|
|
151
|
+
FFmpeg's own `ffmpeg` and `ffprobe` tools are compiled to run inside your app process, so the argument arrays you pass are handled by the real command-line code paths rather than a reimplementation.
|
|
149
152
|
|
|
150
153
|
### Encoders
|
|
151
154
|
|
|
152
|
-
Verified by running the example's device suite
|
|
155
|
+
Verified by running the example's device suite: iOS reports 186 encoders, Android 184. Everything FFmpeg builds natively (`aac`, `alac`, `flac`, `mpeg4`, `mjpeg`, `png`, `gif`, `pcm_*`, β¦) is on both, as are `libmp3lame`, `libopus`, `libvpx`, and `libvpx-vp9`.
|
|
153
156
|
|
|
154
|
-
|
|
157
|
+
H.264 and HEVC come from the platform's hardware encoder, which is faster and smaller than bundling x264 β and keeps the package LGPL:
|
|
155
158
|
|
|
156
159
|
| Encoder | iOS | Android |
|
|
157
160
|
| --- | --- | --- |
|
|
158
|
-
| `libx264` / `libx264rgb` (H.264, software) | β | β
|
|
|
159
|
-
| `libx265` (HEVC, software) | β | β
|
|
|
160
161
|
| `h264_videotoolbox`, `hevc_videotoolbox`, `prores_videotoolbox` | β
| β |
|
|
161
|
-
| `h264_mediacodec`, `hevc_mediacodec`, `vp8_mediacodec`, `vp9_mediacodec
|
|
162
|
-
| `
|
|
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.
|
|
162
|
+
| `h264_mediacodec`, `hevc_mediacodec`, `vp8_mediacodec`, `vp9_mediacodec` | β | β
|
|
|
163
|
+
| `aac_at`, `alac_at` (AudioToolbox) | β
| β |
|
|
168
164
|
|
|
169
|
-
|
|
165
|
+
Resolve the name at runtime instead of branching on `Platform.OS`:
|
|
170
166
|
|
|
171
167
|
```typescript
|
|
172
168
|
import { execute, pickEncoder } from 'munim-ffmpeg'
|
|
173
169
|
|
|
174
|
-
const h264 = await pickEncoder(['
|
|
170
|
+
const h264 = await pickEncoder(['h264_videotoolbox', 'h264_mediacodec'])
|
|
175
171
|
if (!h264) throw new Error('No H.264 encoder in this build')
|
|
176
172
|
|
|
177
173
|
await execute(['-y', '-i', inputPath, '-c:v', h264, outputPath])
|
|
178
174
|
```
|
|
179
175
|
|
|
180
|
-
|
|
176
|
+
Two things to know about hardware encoders: they want NV12 input on Android (`-pix_fmt nv12`) and planar YUV on iOS, and they reject very small frames β 176Γ144 is the smallest size that works everywhere.
|
|
181
177
|
|
|
182
|
-
|
|
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"`.
|
|
178
|
+
Decoding is uniform: H.264, HEVC, VP8/VP9, AV1, MPEG-4, MP3, AAC, Vorbis, Opus, FLAC and the usual containers, on both platforms. Both link TLS, so `https://` inputs work.
|
|
185
179
|
|
|
186
180
|
## π¦ Installation
|
|
187
181
|
|
|
@@ -201,7 +195,7 @@ pod install
|
|
|
201
195
|
cd ..
|
|
202
196
|
```
|
|
203
197
|
|
|
204
|
-
|
|
198
|
+
FFmpeg and React Native both provide `libc++_shared.so`. Resolve that duplicate in the Android application module:
|
|
205
199
|
|
|
206
200
|
```groovy
|
|
207
201
|
android {
|
|
@@ -229,7 +223,7 @@ The package includes an Expo config plugin. If your project manages its plugin l
|
|
|
229
223
|
}
|
|
230
224
|
```
|
|
231
225
|
|
|
232
|
-
The plugin also configures Android to select one shared C++ runtime when React Native and
|
|
226
|
+
The plugin also configures Android to select one shared C++ runtime when React Native and FFmpeg contribute the same `libc++_shared.so` path.
|
|
233
227
|
|
|
234
228
|
Create a native development build after installation:
|
|
235
229
|
|
|
@@ -244,6 +238,25 @@ You can also create an [EAS development build](https://docs.expo.dev/develop/dev
|
|
|
244
238
|
|
|
245
239
|
> **Important:** `munim-ffmpeg` cannot run in Expo Go because Expo Go does not include this package's native libraries.
|
|
246
240
|
|
|
241
|
+
### Native binaries
|
|
242
|
+
|
|
243
|
+
The FFmpeg libraries are around 200 MB across all six architectures, which does not belong in an npm tarball, so they are downloaded from the matching GitHub release when the package installs and verified against the checksum in `scripts/binaries.json`.
|
|
244
|
+
|
|
245
|
+
If your environment blocks install scripts (`npm install --ignore-scripts`), fetch them explicitly:
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
npx munim-ffmpeg-fetch-binaries
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Behind a proxy or an air-gapped mirror, point the fetcher somewhere else:
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
MUNIM_FFMPEG_BINARIES_URL=https://internal.example.com/munim-ffmpeg-binaries.tar.gz \
|
|
255
|
+
npx munim-ffmpeg-fetch-binaries
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Or build them yourself β see [`scripts/ffmpeg/README.md`](./scripts/ffmpeg/README.md).
|
|
259
|
+
|
|
247
260
|
### Requirements
|
|
248
261
|
|
|
249
262
|
- React Native with the New Architecture enabled
|
|
@@ -580,25 +593,15 @@ if (result.success) {
|
|
|
580
593
|
|
|
581
594
|
## Licensing
|
|
582
595
|
|
|
583
|
-
The JavaScript, TypeScript, Swift, Kotlin, and generated Nitro bridge
|
|
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) |
|
|
596
|
+
The JavaScript, TypeScript, Swift, Kotlin, C core, and generated Nitro bridge in this repository are Apache-2.0.
|
|
589
597
|
|
|
590
|
-
The
|
|
598
|
+
The bundled FFmpeg 9.0.1 is **LGPLv3**, on both platforms. It is configured without `--enable-gpl`, so no x264, x265, xvid, or vid.stab: H.264 and HEVC encoding come from VideoToolbox and MediaCodec instead. The external libraries it does link are LAME (LGPL), Opus (BSD), libvpx (BSD), dav1d (BSD), and mbedTLS (Apache-2.0) on Android.
|
|
591
599
|
|
|
592
|
-
|
|
600
|
+
In practice that means your application does **not** inherit GPL obligations. LGPL still applies: the FFmpeg libraries are linked and their license and notices must be conveyed with your app, and users must be able to relink against a modified FFmpeg. The exact configuration used is recorded in [`scripts/ffmpeg/build-ios.sh`](./scripts/ffmpeg/build-ios.sh) and [`build-android.sh`](./scripts/ffmpeg/build-android.sh), and the binaries can be reproduced from them.
|
|
593
601
|
|
|
594
|
-
|
|
602
|
+
If you need x264 or x265, add `--enable-gpl --enable-libx264 --enable-libx265` to those scripts and rebuild β but then your application does inherit GPLv3.
|
|
595
603
|
|
|
596
|
-
|
|
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.
|
|
604
|
+
See [FFmpeg legal guidance](https://ffmpeg.org/legal.html). This section is an engineering summary, not legal advice.
|
|
602
605
|
|
|
603
606
|
## π Troubleshooting
|
|
604
607
|
|
|
@@ -628,31 +631,15 @@ Inspect `result.success`, `result.cancelled`, `result.returnCode`, `result.outpu
|
|
|
628
631
|
|
|
629
632
|
### iOS pod or build errors
|
|
630
633
|
|
|
631
|
-
Run `pod install` after installation and rebuild from a clean native development build.
|
|
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
|
-
```
|
|
634
|
+
Run `pod install` after installation and rebuild from a clean native development build. If the linker cannot find `MunimFFmpeg.xcframework`, the native binaries were not downloaded β run `npx munim-ffmpeg-fetch-binaries`.
|
|
650
635
|
|
|
651
636
|
### Android build errors
|
|
652
637
|
|
|
653
638
|
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
639
|
|
|
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).
|
|
640
|
+
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).
|
|
641
|
+
|
|
642
|
+
If `System.loadLibrary` cannot find `munimffmpeg9`, the native binaries were not downloaded β run `npx munim-ffmpeg-fetch-binaries`.
|
|
656
643
|
|
|
657
644
|
## Development
|
|
658
645
|
|
|
@@ -677,6 +664,15 @@ npm run example:android
|
|
|
677
664
|
|
|
678
665
|
FFmpeg encoding is slow in a simulator or emulator; run the suite on a physical device.
|
|
679
666
|
|
|
667
|
+
### Rebuilding FFmpeg
|
|
668
|
+
|
|
669
|
+
```bash
|
|
670
|
+
npm run binaries:build # every architecture, ~40 minutes
|
|
671
|
+
npm run binaries:package # bundles them and records the checksum
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
See [`scripts/ffmpeg/README.md`](./scripts/ffmpeg/README.md) for what the build does and the platform quirks it works around.
|
|
675
|
+
|
|
680
676
|
### Releasing
|
|
681
677
|
|
|
682
678
|
Releases run locally from a clean `main`; this repository does not use GitHub Actions.
|
|
@@ -686,7 +682,7 @@ npm run check
|
|
|
686
682
|
npm run release:local
|
|
687
683
|
```
|
|
688
684
|
|
|
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.
|
|
685
|
+
`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. It also uploads `dist-binaries/munim-ffmpeg-binaries.tar.gz` to the GitHub release, which is where `postinstall` fetches it from β so run `npm run binaries:package` first.
|
|
690
686
|
|
|
691
687
|
## π Contributing
|
|
692
688
|
|
package/android/build.gradle
CHANGED
|
@@ -139,9 +139,6 @@ dependencies {
|
|
|
139
139
|
// Add a dependency on NitroModules
|
|
140
140
|
implementation project(":react-native-nitro-modules")
|
|
141
141
|
|
|
142
|
-
//
|
|
143
|
-
//
|
|
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"
|
|
142
|
+
// FFmpeg 9 and the in-process fftools core ship as prebuilt .so files in
|
|
143
|
+
// src/main/jniLibs; see scripts/ffmpeg/ for the build.
|
|
147
144
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
package com.margelo.nitro.munimffmpeg
|
|
2
|
+
|
|
3
|
+
import androidx.annotation.Keep
|
|
4
|
+
import com.facebook.proguard.annotations.DoNotStrip
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Thin wrapper over FFmpeg 9's own command-line tools, compiled to run inside
|
|
8
|
+
* the app process.
|
|
9
|
+
*
|
|
10
|
+
* fftools keeps its parsed command in file-scope globals, so exactly one
|
|
11
|
+
* execution runs at a time; the native core serialises callers and cancels
|
|
12
|
+
* anything still queued when [nativeCancel] is called.
|
|
13
|
+
*/
|
|
14
|
+
@Keep
|
|
15
|
+
@DoNotStrip
|
|
16
|
+
object FFmpegNative {
|
|
17
|
+
init {
|
|
18
|
+
System.loadLibrary("munimffmpeg9")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Return code the tools report when a run was cancelled. */
|
|
22
|
+
const val CANCELLED = 255
|
|
23
|
+
|
|
24
|
+
external fun nativeVersion(): String
|
|
25
|
+
|
|
26
|
+
external fun nativeExecute(arguments: Array<String>, stdoutPath: String): Int
|
|
27
|
+
|
|
28
|
+
external fun nativeExecuteProbe(arguments: Array<String>, outputPath: String): Int
|
|
29
|
+
|
|
30
|
+
external fun nativeCancel()
|
|
31
|
+
|
|
32
|
+
data class Statistics(
|
|
33
|
+
val timeMs: Double,
|
|
34
|
+
val sizeBytes: Double,
|
|
35
|
+
val bitrateKbits: Double,
|
|
36
|
+
val speed: Double,
|
|
37
|
+
val videoFrameNumber: Double,
|
|
38
|
+
val fps: Double,
|
|
39
|
+
val quality: Double,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
@Volatile
|
|
43
|
+
private var logSink: ((String) -> Unit)? = null
|
|
44
|
+
|
|
45
|
+
@Volatile
|
|
46
|
+
private var statisticsSink: ((Statistics) -> Unit)? = null
|
|
47
|
+
|
|
48
|
+
fun <T> withCallbacks(
|
|
49
|
+
onLog: ((String) -> Unit)?,
|
|
50
|
+
onStatistics: ((Statistics) -> Unit)?,
|
|
51
|
+
body: () -> T,
|
|
52
|
+
): T {
|
|
53
|
+
logSink = onLog
|
|
54
|
+
statisticsSink = onStatistics
|
|
55
|
+
try {
|
|
56
|
+
return body()
|
|
57
|
+
} finally {
|
|
58
|
+
logSink = null
|
|
59
|
+
statisticsSink = null
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@JvmStatic
|
|
64
|
+
@Keep
|
|
65
|
+
@DoNotStrip
|
|
66
|
+
fun onLog(message: String) {
|
|
67
|
+
logSink?.invoke(message)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@JvmStatic
|
|
71
|
+
@Keep
|
|
72
|
+
@DoNotStrip
|
|
73
|
+
fun onStatistics(
|
|
74
|
+
timeMs: Double,
|
|
75
|
+
sizeBytes: Double,
|
|
76
|
+
bitrateKbits: Double,
|
|
77
|
+
speed: Double,
|
|
78
|
+
videoFrameNumber: Double,
|
|
79
|
+
fps: Double,
|
|
80
|
+
quality: Double,
|
|
81
|
+
) {
|
|
82
|
+
statisticsSink?.invoke(
|
|
83
|
+
Statistics(timeMs, sizeBytes, bitrateKbits, speed, videoFrameNumber, fps, quality)
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -1,19 +1,39 @@
|
|
|
1
1
|
package com.margelo.nitro.munimffmpeg
|
|
2
2
|
|
|
3
3
|
import androidx.annotation.Keep
|
|
4
|
-
import com.arthenica.ffmpegkit.FFmpegKit
|
|
5
|
-
import com.arthenica.ffmpegkit.FFmpegKitConfig
|
|
6
|
-
import com.arthenica.ffmpegkit.FFprobeKit
|
|
7
|
-
import com.arthenica.ffmpegkit.ReturnCode
|
|
8
|
-
import com.arthenica.ffmpegkit.Session
|
|
9
4
|
import com.facebook.proguard.annotations.DoNotStrip
|
|
10
5
|
import com.margelo.nitro.core.Promise
|
|
6
|
+
import java.io.File
|
|
7
|
+
import java.util.concurrent.atomic.AtomicLong
|
|
11
8
|
|
|
12
9
|
@Keep
|
|
13
10
|
@DoNotStrip
|
|
14
11
|
class HybridMunimFfmpeg : HybridMunimFfmpegSpec() {
|
|
15
12
|
override val ffmpegVersion: String
|
|
16
|
-
get() =
|
|
13
|
+
get() = FFmpegNative.nativeVersion()
|
|
14
|
+
|
|
15
|
+
private val sessions = AtomicLong(0)
|
|
16
|
+
|
|
17
|
+
private fun nextSession() = sessions.incrementAndGet().toDouble()
|
|
18
|
+
|
|
19
|
+
private fun result(
|
|
20
|
+
sessionId: Double,
|
|
21
|
+
returnCode: Int,
|
|
22
|
+
output: String,
|
|
23
|
+
durationMs: Long,
|
|
24
|
+
): FFmpegSessionResult {
|
|
25
|
+
val cancelled = returnCode == FFmpegNative.CANCELLED
|
|
26
|
+
return FFmpegSessionResult(
|
|
27
|
+
sessionId = sessionId,
|
|
28
|
+
returnCode = returnCode.toDouble(),
|
|
29
|
+
success = returnCode == 0,
|
|
30
|
+
cancelled = cancelled,
|
|
31
|
+
state = "completed",
|
|
32
|
+
durationMs = durationMs.toDouble(),
|
|
33
|
+
output = output,
|
|
34
|
+
failStackTrace = null,
|
|
35
|
+
)
|
|
36
|
+
}
|
|
17
37
|
|
|
18
38
|
override fun execute(
|
|
19
39
|
arguments_: Array<String>,
|
|
@@ -29,31 +49,42 @@ class HybridMunimFfmpeg : HybridMunimFfmpegSpec() {
|
|
|
29
49
|
) -> Unit)?,
|
|
30
50
|
onSessionCreated: ((sessionId: Double) -> Unit)?,
|
|
31
51
|
): Promise<FFmpegSessionResult> {
|
|
32
|
-
val
|
|
52
|
+
val sessionId = nextSession()
|
|
53
|
+
onSessionCreated?.invoke(sessionId)
|
|
33
54
|
|
|
34
|
-
|
|
35
|
-
val
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
55
|
+
return Promise.async {
|
|
56
|
+
val output = StringBuilder()
|
|
57
|
+
val startedAt = System.currentTimeMillis()
|
|
58
|
+
// ffmpeg prints reports such as -encoders and -protocols to stdout rather
|
|
59
|
+
// than through its logger, so it is captured to a file and appended.
|
|
60
|
+
val stdout = File.createTempFile("munim-ffmpeg", ".txt")
|
|
61
|
+
|
|
62
|
+
val returnCode = FFmpegNative.withCallbacks(
|
|
63
|
+
onLog = { message ->
|
|
64
|
+
output.append(message)
|
|
65
|
+
onLog?.invoke(message)
|
|
66
|
+
},
|
|
67
|
+
onStatistics = { statistics ->
|
|
40
68
|
onStatistics?.invoke(
|
|
41
|
-
statistics.
|
|
42
|
-
statistics.
|
|
43
|
-
statistics.
|
|
69
|
+
statistics.timeMs,
|
|
70
|
+
statistics.sizeBytes,
|
|
71
|
+
statistics.bitrateKbits,
|
|
44
72
|
statistics.speed,
|
|
45
|
-
statistics.videoFrameNumber
|
|
46
|
-
statistics.
|
|
47
|
-
statistics.
|
|
73
|
+
statistics.videoFrameNumber,
|
|
74
|
+
statistics.fps,
|
|
75
|
+
statistics.quality,
|
|
48
76
|
)
|
|
49
77
|
},
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
78
|
+
) {
|
|
79
|
+
FFmpegNative.nativeExecute(arguments_, stdout.absolutePath)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
val printed = runCatching { stdout.readText() }.getOrDefault("")
|
|
83
|
+
stdout.delete()
|
|
84
|
+
output.append(printed)
|
|
55
85
|
|
|
56
|
-
|
|
86
|
+
result(sessionId, returnCode, output.toString(), System.currentTimeMillis() - startedAt)
|
|
87
|
+
}
|
|
57
88
|
}
|
|
58
89
|
|
|
59
90
|
override fun probe(
|
|
@@ -61,25 +92,19 @@ class HybridMunimFfmpeg : HybridMunimFfmpegSpec() {
|
|
|
61
92
|
onLog: ((message: String) -> Unit)?,
|
|
62
93
|
onSessionCreated: ((sessionId: Double) -> Unit)?,
|
|
63
94
|
): Promise<FFmpegSessionResult> {
|
|
64
|
-
val
|
|
95
|
+
val sessionId = nextSession()
|
|
96
|
+
onSessionCreated?.invoke(sessionId)
|
|
65
97
|
|
|
66
|
-
|
|
67
|
-
val
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
{ log -> onLog?.invoke(log.message) },
|
|
71
|
-
)
|
|
72
|
-
onSessionCreated?.invoke(session.sessionId.toDouble())
|
|
73
|
-
} catch (error: Throwable) {
|
|
74
|
-
promise.reject(error)
|
|
98
|
+
return Promise.async {
|
|
99
|
+
val startedAt = System.currentTimeMillis()
|
|
100
|
+
val (returnCode, report) = runProbe(arguments_, onLog)
|
|
101
|
+
result(sessionId, returnCode, report, System.currentTimeMillis() - startedAt)
|
|
75
102
|
}
|
|
76
|
-
|
|
77
|
-
return promise
|
|
78
103
|
}
|
|
79
104
|
|
|
80
105
|
override fun getMediaInformation(path: String): Promise<String> {
|
|
81
106
|
return Promise.async {
|
|
82
|
-
val
|
|
107
|
+
val (returnCode, report) = runProbe(
|
|
83
108
|
arrayOf(
|
|
84
109
|
"-v",
|
|
85
110
|
"error",
|
|
@@ -90,25 +115,49 @@ class HybridMunimFfmpeg : HybridMunimFfmpegSpec() {
|
|
|
90
115
|
"-show_chapters",
|
|
91
116
|
path,
|
|
92
117
|
),
|
|
118
|
+
null,
|
|
93
119
|
)
|
|
94
|
-
val returnCode = session.returnCode
|
|
95
120
|
|
|
96
|
-
if (
|
|
121
|
+
if (returnCode != 0) {
|
|
97
122
|
throw IllegalStateException(
|
|
98
|
-
|
|
99
|
-
"FFprobe failed with return code ${returnCode?.value ?: -1}"
|
|
100
|
-
},
|
|
123
|
+
report.ifEmpty { "FFprobe failed with return code $returnCode" }
|
|
101
124
|
)
|
|
102
125
|
}
|
|
103
126
|
|
|
104
|
-
|
|
127
|
+
report
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* ffprobe writes its report to stdout, which is not reachable from an app, so
|
|
133
|
+
* it is pointed at a temporary file with `-o` and read back.
|
|
134
|
+
*/
|
|
135
|
+
private fun runProbe(
|
|
136
|
+
arguments: Array<String>,
|
|
137
|
+
onLog: ((message: String) -> Unit)?,
|
|
138
|
+
): Pair<Int, String> {
|
|
139
|
+
val destination = File.createTempFile("munim-ffprobe", ".txt")
|
|
140
|
+
try {
|
|
141
|
+
val logs = StringBuilder()
|
|
142
|
+
val returnCode = FFmpegNative.withCallbacks(
|
|
143
|
+
onLog = { message ->
|
|
144
|
+
logs.append(message)
|
|
145
|
+
onLog?.invoke(message)
|
|
146
|
+
},
|
|
147
|
+
onStatistics = null,
|
|
148
|
+
) {
|
|
149
|
+
FFmpegNative.nativeExecuteProbe(arguments, destination.absolutePath)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
val report = destination.readText()
|
|
153
|
+
return returnCode to report.ifEmpty { logs.toString() }
|
|
154
|
+
} finally {
|
|
155
|
+
destination.delete()
|
|
105
156
|
}
|
|
106
157
|
}
|
|
107
158
|
|
|
108
159
|
override fun cancel(sessionId: Double?) {
|
|
109
|
-
if (sessionId
|
|
110
|
-
FFmpegKit.cancel()
|
|
111
|
-
} else {
|
|
160
|
+
if (sessionId != null) {
|
|
112
161
|
require(
|
|
113
162
|
sessionId.isFinite() &&
|
|
114
163
|
sessionId > 0 &&
|
|
@@ -117,25 +166,13 @@ class HybridMunimFfmpeg : HybridMunimFfmpegSpec() {
|
|
|
117
166
|
) {
|
|
118
167
|
"Invalid FFmpeg session ID: $sessionId. Expected a positive safe integer."
|
|
119
168
|
}
|
|
120
|
-
FFmpegKit.cancel(sessionId.toLong())
|
|
121
169
|
}
|
|
170
|
+
// Only one execution runs at a time, so a targeted cancel and cancelAll()
|
|
171
|
+
// are the same operation.
|
|
172
|
+
FFmpegNative.nativeCancel()
|
|
122
173
|
}
|
|
123
174
|
|
|
124
175
|
override fun cancelAll() {
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
private fun Session.toResult(): FFmpegSessionResult {
|
|
129
|
-
val code = returnCode
|
|
130
|
-
return FFmpegSessionResult(
|
|
131
|
-
sessionId = sessionId.toDouble(),
|
|
132
|
-
returnCode = (code?.value ?: -1).toDouble(),
|
|
133
|
-
success = ReturnCode.isSuccess(code),
|
|
134
|
-
cancelled = ReturnCode.isCancel(code),
|
|
135
|
-
state = state.name.lowercase(),
|
|
136
|
-
durationMs = duration.toDouble(),
|
|
137
|
-
output = output,
|
|
138
|
-
failStackTrace = failStackTrace,
|
|
139
|
-
)
|
|
176
|
+
FFmpegNative.nativeCancel()
|
|
140
177
|
}
|
|
141
178
|
}
|
package/ios/Bridge.h
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
// Bridge.h
|
|
3
3
|
// NitroMunimFfmpeg
|
|
4
4
|
//
|
|
5
|
-
//
|
|
5
|
+
// Objective-C bridging header: exposes the C core that drives FFmpeg 9's
|
|
6
|
+
// in-process command-line tools to the Swift implementation.
|
|
6
7
|
//
|
|
7
8
|
|
|
8
9
|
#pragma once
|
|
10
|
+
|
|
11
|
+
#include "munim_ffmpeg_core.h"
|
|
@@ -1,9 +1,75 @@
|
|
|
1
|
+
import Foundation
|
|
1
2
|
import NitroModules
|
|
2
|
-
|
|
3
|
+
|
|
4
|
+
/// Callbacks for the execution currently holding the core's lock. The core
|
|
5
|
+
/// serialises executions, so a single slot is enough.
|
|
6
|
+
private final class ActiveSession {
|
|
7
|
+
var onLog: ((String) -> Void)?
|
|
8
|
+
var onStatistics: ((Double, Double, Double, Double, Double, Double, Double) -> Void)?
|
|
9
|
+
var output = ""
|
|
10
|
+
|
|
11
|
+
func reset() {
|
|
12
|
+
onLog = nil
|
|
13
|
+
onStatistics = nil
|
|
14
|
+
output = ""
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
private let active = ActiveSession()
|
|
19
|
+
// Concurrent on purpose: the C core serialises executions behind its own lock,
|
|
20
|
+
// and a request that is waiting there can still be cancelled. A serial queue
|
|
21
|
+
// would hold the second call outside the core, where cancelAll() cannot see it.
|
|
22
|
+
private let executionQueue = DispatchQueue(
|
|
23
|
+
label: "com.munimtech.ffmpeg",
|
|
24
|
+
qos: .userInitiated,
|
|
25
|
+
attributes: .concurrent
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
private func installCallbacks() {
|
|
29
|
+
munim_ffmpeg_set_callbacks({ _, message in
|
|
30
|
+
guard let message else { return }
|
|
31
|
+
let text = String(cString: message)
|
|
32
|
+
active.output += text
|
|
33
|
+
active.onLog?(text)
|
|
34
|
+
}, { _, timeMs, sizeBytes, bitrate, speed, frame, fps, quality in
|
|
35
|
+
active.onStatistics?(timeMs, sizeBytes, bitrate, speed, frame, fps, quality)
|
|
36
|
+
}, nil)
|
|
37
|
+
}
|
|
3
38
|
|
|
4
39
|
final class HybridMunimFfmpeg: HybridMunimFfmpegSpec {
|
|
40
|
+
private var sessionCounter: Double = 0
|
|
41
|
+
|
|
5
42
|
var ffmpegVersion: String {
|
|
6
|
-
|
|
43
|
+
String(cString: munim_ffmpeg_version())
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private func nextSession() -> Double {
|
|
47
|
+
sessionCounter += 1
|
|
48
|
+
return sessionCounter
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private static func temporaryFile() -> String {
|
|
52
|
+
FileManager.default.temporaryDirectory
|
|
53
|
+
.appendingPathComponent("munim-ffmpeg-\(UUID().uuidString)")
|
|
54
|
+
.path
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private func result(
|
|
58
|
+
sessionId: Double,
|
|
59
|
+
returnCode: Int32,
|
|
60
|
+
output: String,
|
|
61
|
+
startedAt: Date
|
|
62
|
+
) -> FFmpegSessionResult {
|
|
63
|
+
FFmpegSessionResult(
|
|
64
|
+
sessionId: sessionId,
|
|
65
|
+
returnCode: Double(returnCode),
|
|
66
|
+
success: returnCode == 0,
|
|
67
|
+
cancelled: returnCode == Int32(MUNIM_FFMPEG_CANCELLED),
|
|
68
|
+
state: "completed",
|
|
69
|
+
durationMs: Date().timeIntervalSince(startedAt) * 1000,
|
|
70
|
+
output: output,
|
|
71
|
+
failStackTrace: nil
|
|
72
|
+
)
|
|
7
73
|
}
|
|
8
74
|
|
|
9
75
|
func execute(
|
|
@@ -13,35 +79,38 @@ final class HybridMunimFfmpeg: HybridMunimFfmpegSpec {
|
|
|
13
79
|
onSessionCreated: ((_ sessionId: Double) -> Void)?
|
|
14
80
|
) throws -> Promise<FFmpegSessionResult> {
|
|
15
81
|
let promise = Promise<FFmpegSessionResult>()
|
|
82
|
+
let sessionId = nextSession()
|
|
83
|
+
onSessionCreated?(sessionId)
|
|
84
|
+
|
|
85
|
+
executionQueue.async {
|
|
86
|
+
let startedAt = Date()
|
|
87
|
+
// ffmpeg prints reports such as -encoders to stdout rather than through
|
|
88
|
+
// its logger, so it is captured to a file and appended to the output.
|
|
89
|
+
let printedPath = Self.temporaryFile()
|
|
90
|
+
|
|
91
|
+
installCallbacks()
|
|
92
|
+
active.reset()
|
|
93
|
+
active.onLog = onLog
|
|
94
|
+
active.onStatistics = onStatistics
|
|
95
|
+
|
|
96
|
+
let returnCode = withArrayOfCStrings(arguments_) { argv in
|
|
97
|
+
munim_ffmpeg_execute(Int32(arguments_.count), argv, printedPath)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let printed = (try? String(contentsOfFile: printedPath, encoding: .utf8)) ?? ""
|
|
101
|
+
try? FileManager.default.removeItem(atPath: printedPath)
|
|
16
102
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
withLogCallback: { log in
|
|
27
|
-
guard let message = log?.getMessage() else { return }
|
|
28
|
-
onLog?(message)
|
|
29
|
-
},
|
|
30
|
-
withStatisticsCallback: { statistics in
|
|
31
|
-
guard let statistics else { return }
|
|
32
|
-
onStatistics?(
|
|
33
|
-
statistics.getTime(),
|
|
34
|
-
Double(statistics.getSize()),
|
|
35
|
-
statistics.getBitrate(),
|
|
36
|
-
statistics.getSpeed(),
|
|
37
|
-
Double(statistics.getVideoFrameNumber()),
|
|
38
|
-
Double(statistics.getVideoFps()),
|
|
39
|
-
Double(statistics.getVideoQuality())
|
|
103
|
+
let output = active.output + printed
|
|
104
|
+
active.reset()
|
|
105
|
+
|
|
106
|
+
promise.resolve(
|
|
107
|
+
withResult: self.result(
|
|
108
|
+
sessionId: sessionId,
|
|
109
|
+
returnCode: returnCode,
|
|
110
|
+
output: output,
|
|
111
|
+
startedAt: startedAt
|
|
40
112
|
)
|
|
41
|
-
|
|
42
|
-
)
|
|
43
|
-
if let session {
|
|
44
|
-
onSessionCreated?(Double(session.getId()))
|
|
113
|
+
)
|
|
45
114
|
}
|
|
46
115
|
|
|
47
116
|
return promise
|
|
@@ -53,23 +122,20 @@ final class HybridMunimFfmpeg: HybridMunimFfmpegSpec {
|
|
|
53
122
|
onSessionCreated: ((_ sessionId: Double) -> Void)?
|
|
54
123
|
) throws -> Promise<FFmpegSessionResult> {
|
|
55
124
|
let promise = Promise<FFmpegSessionResult>()
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
)
|
|
71
|
-
if let session {
|
|
72
|
-
onSessionCreated?(Double(session.getId()))
|
|
125
|
+
let sessionId = nextSession()
|
|
126
|
+
onSessionCreated?(sessionId)
|
|
127
|
+
|
|
128
|
+
executionQueue.async {
|
|
129
|
+
let startedAt = Date()
|
|
130
|
+
let (returnCode, report) = Self.runProbe(arguments_, onLog: onLog)
|
|
131
|
+
promise.resolve(
|
|
132
|
+
withResult: self.result(
|
|
133
|
+
sessionId: sessionId,
|
|
134
|
+
returnCode: returnCode,
|
|
135
|
+
output: report,
|
|
136
|
+
startedAt: startedAt
|
|
137
|
+
)
|
|
138
|
+
)
|
|
73
139
|
}
|
|
74
140
|
|
|
75
141
|
return promise
|
|
@@ -77,37 +143,58 @@ final class HybridMunimFfmpeg: HybridMunimFfmpegSpec {
|
|
|
77
143
|
|
|
78
144
|
func getMediaInformation(path: String) throws -> Promise<String> {
|
|
79
145
|
let promise = Promise<String>()
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
guard ReturnCode.isSuccess(returnCode) else {
|
|
100
|
-
let message = session.getFailStackTrace() ?? session.getOutput() ?? "FFprobe failed"
|
|
101
|
-
promise.reject(withError: MunimFfmpegError.executionFailed(message))
|
|
102
|
-
return
|
|
103
|
-
}
|
|
104
|
-
promise.resolve(withResult: session.getOutput() ?? "{}")
|
|
146
|
+
|
|
147
|
+
executionQueue.async {
|
|
148
|
+
let (returnCode, report) = Self.runProbe(
|
|
149
|
+
[
|
|
150
|
+
"-v", "error",
|
|
151
|
+
"-print_format", "json",
|
|
152
|
+
"-show_format", "-show_streams", "-show_chapters",
|
|
153
|
+
path,
|
|
154
|
+
],
|
|
155
|
+
onLog: nil
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
if returnCode != 0 {
|
|
159
|
+
promise.reject(
|
|
160
|
+
withError: MunimFfmpegError.executionFailed(
|
|
161
|
+
report.isEmpty ? "FFprobe failed with return code \(returnCode)" : report
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
return
|
|
105
165
|
}
|
|
106
|
-
|
|
166
|
+
|
|
167
|
+
promise.resolve(withResult: report)
|
|
168
|
+
}
|
|
107
169
|
|
|
108
170
|
return promise
|
|
109
171
|
}
|
|
110
172
|
|
|
173
|
+
/// ffprobe writes its report to stdout, which is not reachable from an app,
|
|
174
|
+
/// so it is pointed at a temporary file with `-o` and read back.
|
|
175
|
+
private static func runProbe(
|
|
176
|
+
_ arguments: [String],
|
|
177
|
+
onLog: ((String) -> Void)?
|
|
178
|
+
) -> (Int32, String) {
|
|
179
|
+
let destination = temporaryFile()
|
|
180
|
+
|
|
181
|
+
installCallbacks()
|
|
182
|
+
active.reset()
|
|
183
|
+
active.onLog = onLog
|
|
184
|
+
|
|
185
|
+
let returnCode = withArrayOfCStrings(arguments) { argv in
|
|
186
|
+
munim_ffmpeg_probe(Int32(arguments.count), argv, destination)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let report = (try? String(contentsOfFile: destination, encoding: .utf8)) ?? ""
|
|
190
|
+
try? FileManager.default.removeItem(atPath: destination)
|
|
191
|
+
|
|
192
|
+
let logs = active.output
|
|
193
|
+
active.reset()
|
|
194
|
+
|
|
195
|
+
return (returnCode, report.isEmpty ? logs : report)
|
|
196
|
+
}
|
|
197
|
+
|
|
111
198
|
func cancel(sessionId: Double?) throws {
|
|
112
199
|
if let sessionId {
|
|
113
200
|
guard
|
|
@@ -118,52 +205,39 @@ final class HybridMunimFfmpeg: HybridMunimFfmpegSpec {
|
|
|
118
205
|
else {
|
|
119
206
|
throw MunimFfmpegError.invalidSessionId(sessionId)
|
|
120
207
|
}
|
|
121
|
-
FFmpegKit.cancel(Int(sessionId))
|
|
122
|
-
} else {
|
|
123
|
-
FFmpegKit.cancel()
|
|
124
208
|
}
|
|
209
|
+
// One execution runs at a time, so cancelling a specific session and
|
|
210
|
+
// cancelling everything are the same operation.
|
|
211
|
+
munim_ffmpeg_cancel()
|
|
125
212
|
}
|
|
126
213
|
|
|
127
214
|
func cancelAll() throws {
|
|
128
|
-
|
|
129
|
-
}
|
|
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
|
-
}
|
|
215
|
+
munim_ffmpeg_cancel()
|
|
141
216
|
}
|
|
217
|
+
}
|
|
142
218
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
219
|
+
/// Builds a C `argv` that stays valid for the duration of `body`.
|
|
220
|
+
private func withArrayOfCStrings<R>(
|
|
221
|
+
_ values: [String],
|
|
222
|
+
_ body: (UnsafePointer<UnsafePointer<CChar>?>?) -> R
|
|
223
|
+
) -> R {
|
|
224
|
+
var pointers = values.map { strdup($0) }
|
|
225
|
+
defer { pointers.forEach { free($0) } }
|
|
226
|
+
|
|
227
|
+
return pointers.withUnsafeMutableBufferPointer { buffer in
|
|
228
|
+
buffer.baseAddress!.withMemoryRebound(
|
|
229
|
+
to: UnsafePointer<CChar>?.self,
|
|
230
|
+
capacity: buffer.count
|
|
231
|
+
) { body($0) }
|
|
155
232
|
}
|
|
156
233
|
}
|
|
157
234
|
|
|
158
235
|
private enum MunimFfmpegError: LocalizedError {
|
|
159
|
-
case missingSession
|
|
160
236
|
case executionFailed(String)
|
|
161
237
|
case invalidSessionId(Double)
|
|
162
238
|
|
|
163
239
|
var errorDescription: String? {
|
|
164
240
|
switch self {
|
|
165
|
-
case .missingSession:
|
|
166
|
-
return "FFmpegKit did not return a session."
|
|
167
241
|
case .executionFailed(let message):
|
|
168
242
|
return message
|
|
169
243
|
case .invalidSessionId(let sessionId):
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Platform-neutral core for running FFmpeg 9's own command-line tools inside an
|
|
3
|
+
* app process. The Android JNI bridge and the iOS Swift layer both sit on top
|
|
4
|
+
* of this.
|
|
5
|
+
*/
|
|
6
|
+
#ifndef MUNIM_FFMPEG_CORE_H
|
|
7
|
+
#define MUNIM_FFMPEG_CORE_H
|
|
8
|
+
|
|
9
|
+
#ifdef __cplusplus
|
|
10
|
+
extern "C" {
|
|
11
|
+
#endif
|
|
12
|
+
|
|
13
|
+
typedef void (*munim_log_callback)(void *context, const char *message);
|
|
14
|
+
|
|
15
|
+
typedef void (*munim_statistics_callback)(void *context, double time_ms,
|
|
16
|
+
double size_bytes,
|
|
17
|
+
double bitrate_kbits, double speed,
|
|
18
|
+
double video_frame_number, double fps,
|
|
19
|
+
double quality);
|
|
20
|
+
|
|
21
|
+
/** Version string of the linked FFmpeg, e.g. "9.0.1". */
|
|
22
|
+
const char *munim_ffmpeg_version(void);
|
|
23
|
+
|
|
24
|
+
/** Callbacks apply to whichever execution is currently running. */
|
|
25
|
+
void munim_ffmpeg_set_callbacks(munim_log_callback on_log,
|
|
26
|
+
munim_statistics_callback on_statistics,
|
|
27
|
+
void *context);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Runs `ffmpeg` with the given arguments; `argv[0]` is supplied internally.
|
|
31
|
+
*
|
|
32
|
+
* `stdout_path` receives anything the tool prints rather than logs, such as the
|
|
33
|
+
* `-encoders` and `-protocols` reports. Pass NULL to discard it.
|
|
34
|
+
*
|
|
35
|
+
* fftools keeps its parsed command in file-scope globals, so calls are
|
|
36
|
+
* serialised: a second execution waits for the first to finish.
|
|
37
|
+
*/
|
|
38
|
+
int munim_ffmpeg_execute(int argc, const char *const *argv,
|
|
39
|
+
const char *stdout_path);
|
|
40
|
+
|
|
41
|
+
/** Runs `ffprobe`, writing its report to `output_path` via `-o`. */
|
|
42
|
+
int munim_ffmpeg_probe(int argc, const char *const *argv,
|
|
43
|
+
const char *output_path);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Requests cancellation of the running execution, and of any execution already
|
|
47
|
+
* queued behind it.
|
|
48
|
+
*/
|
|
49
|
+
void munim_ffmpeg_cancel(void);
|
|
50
|
+
|
|
51
|
+
/** Return code reported when a run was cancelled. */
|
|
52
|
+
#define MUNIM_FFMPEG_CANCELLED 255
|
|
53
|
+
|
|
54
|
+
#ifdef __cplusplus
|
|
55
|
+
}
|
|
56
|
+
#endif
|
|
57
|
+
|
|
58
|
+
#endif /* MUNIM_FFMPEG_CORE_H */
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "munim-ffmpeg",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|
|
7
7
|
"types": "lib/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"munim-ffmpeg-fetch-binaries": "scripts/fetch-binaries.mjs"
|
|
10
|
+
},
|
|
8
11
|
"react-native": "src/index",
|
|
9
12
|
"source": "src/index",
|
|
10
13
|
"files": [
|
|
@@ -16,7 +19,7 @@
|
|
|
16
19
|
"android/gradle.properties",
|
|
17
20
|
"android/fix-prefab.gradle",
|
|
18
21
|
"android/CMakeLists.txt",
|
|
19
|
-
"android/src",
|
|
22
|
+
"android/src/main/java",
|
|
20
23
|
"cpp",
|
|
21
24
|
"ios/**/*.h",
|
|
22
25
|
"ios/**/*.m",
|
|
@@ -28,7 +31,10 @@
|
|
|
28
31
|
"*.podspec",
|
|
29
32
|
"README.md",
|
|
30
33
|
"LICENSE",
|
|
31
|
-
"
|
|
34
|
+
"android/src/main/cpp",
|
|
35
|
+
"android/src/main/AndroidManifest.xml",
|
|
36
|
+
"scripts/fetch-binaries.mjs",
|
|
37
|
+
"scripts/binaries.json"
|
|
32
38
|
],
|
|
33
39
|
"scripts": {
|
|
34
40
|
"typecheck": "tsc --noEmit",
|
|
@@ -44,7 +50,10 @@
|
|
|
44
50
|
"example:ios": "npm --workspace example run ios",
|
|
45
51
|
"example:android": "npm --workspace example run android",
|
|
46
52
|
"release:local": "node scripts/release-local.mjs",
|
|
47
|
-
"format": "prettier --write \"**/*.{ts,tsx,js,cjs,mjs,json,md}\""
|
|
53
|
+
"format": "prettier --write \"**/*.{ts,tsx,js,cjs,mjs,json,md}\"",
|
|
54
|
+
"postinstall": "node scripts/fetch-binaries.mjs",
|
|
55
|
+
"binaries:build": "scripts/ffmpeg/fetch-source.sh && scripts/ffmpeg/build-all.sh",
|
|
56
|
+
"binaries:package": "scripts/ffmpeg/package.sh"
|
|
48
57
|
},
|
|
49
58
|
"keywords": [
|
|
50
59
|
"react-native",
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* Downloads the prebuilt FFmpeg binaries for this version of munim-ffmpeg.
|
|
4
|
+
*
|
|
5
|
+
* They are not published to npm: the bundle is ~200 MB of static libraries and
|
|
6
|
+
* shared objects, which does not belong in a registry tarball. It ships as a
|
|
7
|
+
* GitHub release asset instead, pinned by the checksum in scripts/binaries.json
|
|
8
|
+
* so a build cannot silently pick up different bytes.
|
|
9
|
+
*
|
|
10
|
+
* Runs from postinstall, and can be re-run by hand:
|
|
11
|
+
* npx munim-ffmpeg-fetch-binaries
|
|
12
|
+
*/
|
|
13
|
+
import { createHash } from 'node:crypto'
|
|
14
|
+
import { createWriteStream } from 'node:fs'
|
|
15
|
+
import { mkdir, readFile, rm, stat } from 'node:fs/promises'
|
|
16
|
+
import { createRequire } from 'node:module'
|
|
17
|
+
import path from 'node:path'
|
|
18
|
+
import { pipeline } from 'node:stream/promises'
|
|
19
|
+
import { fileURLToPath } from 'node:url'
|
|
20
|
+
import { execFile } from 'node:child_process'
|
|
21
|
+
import { promisify } from 'node:util'
|
|
22
|
+
|
|
23
|
+
const run = promisify(execFile)
|
|
24
|
+
const require = createRequire(import.meta.url)
|
|
25
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
26
|
+
|
|
27
|
+
const REPOSITORY = 'munimtechnologies/munim-ffmpeg'
|
|
28
|
+
const MARKERS = [
|
|
29
|
+
'ios/MunimFFmpeg.xcframework',
|
|
30
|
+
'android/src/main/jniLibs/arm64-v8a/libmunimffmpeg9.so',
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
async function exists(target) {
|
|
34
|
+
try {
|
|
35
|
+
await stat(target)
|
|
36
|
+
return true
|
|
37
|
+
} catch {
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function alreadyInstalled() {
|
|
43
|
+
for (const marker of MARKERS) {
|
|
44
|
+
if (!(await exists(path.join(root, marker)))) return false
|
|
45
|
+
}
|
|
46
|
+
return true
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function download(url, destination) {
|
|
50
|
+
const response = await fetch(url, { redirect: 'follow' })
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
throw new Error(`${response.status} ${response.statusText} for ${url}`)
|
|
53
|
+
}
|
|
54
|
+
await pipeline(response.body, createWriteStream(destination))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function checksum(file) {
|
|
58
|
+
const hash = createHash('sha256')
|
|
59
|
+
hash.update(await readFile(file))
|
|
60
|
+
return hash.digest('hex')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function main() {
|
|
64
|
+
const manifest = require(path.join(root, 'scripts', 'binaries.json'))
|
|
65
|
+
const { version } = require(path.join(root, 'package.json'))
|
|
66
|
+
|
|
67
|
+
if (await alreadyInstalled()) {
|
|
68
|
+
console.log(
|
|
69
|
+
`munim-ffmpeg: FFmpeg ${manifest.ffmpeg} binaries already present`
|
|
70
|
+
)
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const url =
|
|
75
|
+
process.env.MUNIM_FFMPEG_BINARIES_URL ??
|
|
76
|
+
`https://github.com/${REPOSITORY}/releases/download/v${version}/${manifest.archive}`
|
|
77
|
+
|
|
78
|
+
const cache = path.join(root, '.binaries-cache')
|
|
79
|
+
await mkdir(cache, { recursive: true })
|
|
80
|
+
const archive = path.join(cache, manifest.archive)
|
|
81
|
+
|
|
82
|
+
console.log(`munim-ffmpeg: downloading FFmpeg ${manifest.ffmpeg} binariesβ¦`)
|
|
83
|
+
await download(url, archive)
|
|
84
|
+
|
|
85
|
+
const actual = await checksum(archive)
|
|
86
|
+
if (actual !== manifest.sha256) {
|
|
87
|
+
await rm(cache, { force: true, recursive: true })
|
|
88
|
+
throw new Error(
|
|
89
|
+
`checksum mismatch for ${manifest.archive}\n expected ${manifest.sha256}\n received ${actual}`
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
await run('tar', ['xzf', archive, '-C', root])
|
|
94
|
+
await rm(cache, { force: true, recursive: true })
|
|
95
|
+
|
|
96
|
+
if (!(await alreadyInstalled())) {
|
|
97
|
+
throw new Error('the archive did not contain the expected binaries')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log(`munim-ffmpeg: FFmpeg ${manifest.ffmpeg} binaries installed`)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
main().catch((error) => {
|
|
104
|
+
console.error(`
|
|
105
|
+
munim-ffmpeg: could not install the native FFmpeg binaries.
|
|
106
|
+
|
|
107
|
+
${error.message}
|
|
108
|
+
|
|
109
|
+
The package cannot build without them. Options:
|
|
110
|
+
β’ re-run: npx munim-ffmpeg-fetch-binaries
|
|
111
|
+
β’ use a mirror: MUNIM_FFMPEG_BINARIES_URL=<url> npx munim-ffmpeg-fetch-binaries
|
|
112
|
+
β’ build them: see scripts/ffmpeg/README.md
|
|
113
|
+
`)
|
|
114
|
+
process.exitCode = 1
|
|
115
|
+
})
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'fileutils'
|
|
4
|
-
require 'tempfile'
|
|
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.
|
|
15
|
-
pods_root = ENV.fetch('PODS_ROOT')
|
|
16
|
-
build_products = ENV.fetch('PODS_CONFIGURATION_BUILD_DIR')
|
|
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) }
|
|
22
|
-
|
|
23
|
-
abort('munim-ffmpeg: could not find the expected FFmpegKit Level.h') if headers.empty?
|
|
24
|
-
|
|
25
|
-
unsigned_declaration = 'NS_ENUM(NSUInteger, Level)'
|
|
26
|
-
signed_declaration = 'NS_ENUM(NSInteger, Level)'
|
|
27
|
-
|
|
28
|
-
headers.each do |header|
|
|
29
|
-
contents = File.binread(header)
|
|
30
|
-
next if contents.include?(signed_declaration) && !contents.include?(unsigned_declaration)
|
|
31
|
-
|
|
32
|
-
occurrences = contents.scan(unsigned_declaration).length
|
|
33
|
-
abort("munim-ffmpeg: unexpected Level enum declaration in #{header}") unless occurrences == 1
|
|
34
|
-
|
|
35
|
-
patched = contents.sub(unsigned_declaration, signed_declaration)
|
|
36
|
-
Tempfile.create(['Level', '.h'], File.dirname(header)) do |temporary|
|
|
37
|
-
temporary.binmode
|
|
38
|
-
temporary.write(patched)
|
|
39
|
-
temporary.flush
|
|
40
|
-
FileUtils.chmod(File.stat(header).mode, temporary.path)
|
|
41
|
-
FileUtils.mv(temporary.path, header)
|
|
42
|
-
end
|
|
43
|
-
end
|