munim-ffmpeg 0.1.1 โ†’ 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.
@@ -18,24 +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-https-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
- s.script_phase = {
35
- :name => 'Patch FFmpegKit Level enum for Xcode 26',
36
- :script => '"${RUBY_EXECUTABLE:-/usr/bin/ruby}" "${PODS_TARGET_SRCROOT}/scripts/patch-ffmpegkit-level.rb"',
37
- :execution_position => :before_compile,
38
- }
39
45
 
40
46
  install_modules_dependencies(s)
41
47
  end
package/README.md CHANGED
@@ -61,19 +61,20 @@
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 Android artifact is GPL-enabled and includes x264/x265; distributing it can trigger GPLv3 obligations. The native dependencies and FFmpeg retain their own licenses. Review [Native dependencies and licensing](#native-dependencies-and-licensing) before distributing an app.
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
 
68
68
  - [๐Ÿ“š Documentation](#-documentation)
69
69
  - [๐Ÿš€ Features](#-features)
70
70
  - [Platform support matrix](#platform-support-matrix)
71
+ - [Bundled FFmpeg builds](#bundled-ffmpeg-builds)
71
72
  - [๐Ÿ“ฆ Installation](#-installation)
72
73
  - [Working with media paths](#working-with-media-paths)
73
74
  - [โšก Quick start](#-quick-start)
74
75
  - [๐Ÿ”ง API reference](#-api-reference)
75
76
  - [๐Ÿ“– Usage examples](#-usage-examples)
76
- - [Native dependencies and licensing](#native-dependencies-and-licensing)
77
+ - [Licensing](#licensing)
77
78
  - [๐Ÿ” Troubleshooting](#-troubleshooting)
78
79
  - [Development](#development)
79
80
  - [๐Ÿ‘ Contributing](#-contributing)
@@ -91,7 +92,8 @@
91
92
 
92
93
  ### FFmpeg execution
93
94
 
94
- - ๐ŸŽฌ **Argument-array commands:** Avoid platform-specific shell parsing and quoting
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
95
97
  - โšก **Asynchronous sessions:** Keep the React Native thread responsive during native work
96
98
  - ๐Ÿ“ **Live logs:** Receive FFmpeg output as it is produced
97
99
  - ๐Ÿ“ˆ **Encoding statistics:** Track time, size, bitrate, speed, frames, FPS, and quality
@@ -109,8 +111,9 @@
109
111
  - ๐Ÿ“ฑ **iOS and Android:** Native implementations in Swift and Kotlin
110
112
  - ๐Ÿงฌ **Nitro Modules:** Generated high-performance native bindings
111
113
  - ๐Ÿš€ **Expo compatible:** Autolinking, config plugin, and an Expo development example
114
+ - ๐Ÿงช **Capability discovery:** Ask the bundled build which encoders and decoders it actually has
112
115
  - ๐ŸŽฏ **TypeScript:** Complete public callback and result types
113
- - ๐Ÿ—‚๏ธ **16 KB Android support:** Uses a maintained FFmpegKit-compatible Android artifact with 16 KB page-size support
116
+ - ๐Ÿ—‚๏ธ **16 KB Android pages:** Built with the alignment Google Play requires
114
117
 
115
118
  ## Platform support matrix
116
119
 
@@ -125,9 +128,54 @@
125
128
  | Cancel one FFmpeg session | โœ… | โœ… | Pass the positive safe-integer ID received by `execute`'s `onSessionCreated`. The native dependency does not expose FFprobe cancellation. |
126
129
  | Cancel all FFmpeg sessions | โœ… | โœ… | Use `cancelAll()` or call `cancel()` without an ID. |
127
130
  | Expo Go | โŒ | โŒ | A native development build is required. |
128
- | Remote HTTP(S) inputs | Build-dependent | Build-dependent | The bundled variants include HTTPS support, but remote server behavior and protocol support can vary. Prefer local files for predictable app workflows. |
131
+ | Capability discovery | โœ… | โœ… | `listEncoders()`, `listDecoders()`, and `pickEncoder()` report what the bundled build supports. |
132
+ | H.264 encoding | VideoToolbox | libx264 | The builds differ; use `pickEncoder(['libx264', 'h264_videotoolbox'])` instead of hard-coding an encoder. |
133
+ | Remote HTTP(S) inputs | โœ… | โœ… | Both builds link GnuTLS. Remote server behaviour still varies; prefer local files for predictable app workflows. |
129
134
 
130
- Codec availability is determined by the native FFmpeg builds listed below. Do not assume every FFmpeg codec or external library is bundled.
135
+ 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.
136
+
137
+ ## Bundled FFmpeg builds
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.
140
+
141
+ | | iOS | Android |
142
+ | --- | --- | --- |
143
+ | FFmpeg | 9.0.1 | 9.0.1 |
144
+ | Architectures | arm64 device, arm64 + x86_64 simulator | arm64-v8a, armeabi-v7a, x86_64 |
145
+ | Hardware codecs | VideoToolbox, AudioToolbox | MediaCodec |
146
+ | TLS | SecureTransport | mbedTLS |
147
+ | Minimum | iOS 15.1 | API 24, 16 KB pages |
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.
152
+
153
+ ### Encoders
154
+
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`.
156
+
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:
158
+
159
+ | Encoder | iOS | Android |
160
+ | --- | --- | --- |
161
+ | `h264_videotoolbox`, `hevc_videotoolbox`, `prores_videotoolbox` | โœ… | โŒ |
162
+ | `h264_mediacodec`, `hevc_mediacodec`, `vp8_mediacodec`, `vp9_mediacodec` | โŒ | โœ… |
163
+ | `aac_at`, `alac_at` (AudioToolbox) | โœ… | โŒ |
164
+
165
+ Resolve the name at runtime instead of branching on `Platform.OS`:
166
+
167
+ ```typescript
168
+ import { execute, pickEncoder } from 'munim-ffmpeg'
169
+
170
+ const h264 = await pickEncoder(['h264_videotoolbox', 'h264_mediacodec'])
171
+ if (!h264) throw new Error('No H.264 encoder in this build')
172
+
173
+ await execute(['-y', '-i', inputPath, '-c:v', h264, outputPath])
174
+ ```
175
+
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.
177
+
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.
131
179
 
132
180
  ## ๐Ÿ“ฆ Installation
133
181
 
@@ -147,7 +195,7 @@ pod install
147
195
  cd ..
148
196
  ```
149
197
 
150
- FFmpegKit and React Native both provide `libc++_shared.so`. Resolve that duplicate in the Android application module:
198
+ FFmpeg and React Native both provide `libc++_shared.so`. Resolve that duplicate in the Android application module:
151
199
 
152
200
  ```groovy
153
201
  android {
@@ -175,7 +223,7 @@ The package includes an Expo config plugin. If your project manages its plugin l
175
223
  }
176
224
  ```
177
225
 
178
- The plugin also configures Android to select one shared C++ runtime when React Native and FFmpegKit contribute the same `libc++_shared.so` path.
226
+ The plugin also configures Android to select one shared C++ runtime when React Native and FFmpeg contribute the same `libc++_shared.so` path.
179
227
 
180
228
  Create a native development build after installation:
181
229
 
@@ -190,6 +238,25 @@ You can also create an [EAS development build](https://docs.expo.dev/develop/dev
190
238
 
191
239
  > **Important:** `munim-ffmpeg` cannot run in Expo Go because Expo Go does not include this package's native libraries.
192
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
+
193
260
  ### Requirements
194
261
 
195
262
  - React Native with the New Architecture enabled
@@ -205,7 +272,7 @@ No camera, microphone, photo-library, or storage permission is added automatical
205
272
  FFmpeg runs natively and needs a path or URI the native process can access.
206
273
 
207
274
  - Prefer files inside your app's document, cache, or temporary directory.
208
- - `file://` URIs and plain local paths are the most predictable inputs and outputs.
275
+ - `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`.
209
276
  - On Android, copy a `content://` document into application storage before processing when the native library cannot open it directly.
210
277
  - Copy photo-library or document-picker assets when the provider gives temporary or security-scoped access.
211
278
  - Ensure the output directory already exists.
@@ -343,6 +410,44 @@ Returns the version reported by the bundled native FFmpeg library.
343
410
  function getFFmpegVersion(): string
344
411
  ```
345
412
 
413
+ ### `listEncoders()`
414
+
415
+ Returns the encoder names the bundled FFmpeg build can write. The result is cached after the first call.
416
+
417
+ ```typescript
418
+ function listEncoders(): Promise<string[]>
419
+ ```
420
+
421
+ ### `listDecoders()`
422
+
423
+ Returns the decoder names the bundled FFmpeg build can read.
424
+
425
+ ```typescript
426
+ function listDecoders(): Promise<string[]>
427
+ ```
428
+
429
+ ### `pickEncoder(candidates)`
430
+
431
+ 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.
432
+
433
+ ```typescript
434
+ function pickEncoder(candidates: string[]): Promise<string | undefined>
435
+ ```
436
+
437
+ ```typescript
438
+ const hevc = await pickEncoder(['libx265', 'hevc_videotoolbox'])
439
+ ```
440
+
441
+ ### `normalizePath(value)`
442
+
443
+ Converts a `file://` URI into the plain path FFmpeg expects, and returns anything else untouched.
444
+
445
+ ```typescript
446
+ function normalizePath(value: string): string
447
+ ```
448
+
449
+ `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.
450
+
346
451
  ### `FFmpegSessionResult`
347
452
 
348
453
  ```typescript
@@ -358,6 +463,8 @@ type FFmpegSessionResult = {
358
463
  }
359
464
  ```
360
465
 
466
+ `state` is one of `created`, `running`, `failed`, or `completed`, and reports the same values on both platforms.
467
+
361
468
  Always check `success` or `cancelled`; Promise resolution means the native session completed, not necessarily that FFmpeg returned a success code.
362
469
 
363
470
  ## ๐Ÿ“– Usage examples
@@ -391,6 +498,34 @@ if (!result.success) {
391
498
  }
392
499
  ```
393
500
 
501
+ ### Transcode to H.264 on both platforms
502
+
503
+ ```typescript
504
+ import { execute, pickEncoder } from 'munim-ffmpeg'
505
+
506
+ const encoder = await pickEncoder(['libx264', 'h264_videotoolbox'])
507
+ if (!encoder) throw new Error('No H.264 encoder available in this build')
508
+
509
+ // -preset is an x264 option; VideoToolbox rejects it.
510
+ const quality = encoder === 'libx264' ? ['-preset', 'veryfast', '-crf', '23'] : ['-b:v', '2M']
511
+
512
+ const result = await execute([
513
+ '-y',
514
+ '-i',
515
+ inputPath,
516
+ '-c:v',
517
+ encoder,
518
+ ...quality,
519
+ '-c:a',
520
+ 'aac',
521
+ '-pix_fmt',
522
+ 'yuv420p',
523
+ outputPath,
524
+ ])
525
+
526
+ if (!result.success) throw new Error(result.failStackTrace ?? result.output)
527
+ ```
528
+
394
529
  ### Generate a thumbnail
395
530
 
396
531
  ```typescript
@@ -456,25 +591,17 @@ if (result.success) {
456
591
  }
457
592
  ```
458
593
 
459
- ## Native dependencies and licensing
460
-
461
- The JavaScript, TypeScript, Swift, Kotlin, and generated Nitro bridge code in this repository are Apache-2.0 licensed. Native FFmpeg binaries are supplied by separate compatibility packages:
594
+ ## Licensing
462
595
 
463
- | Platform | Native dependency | Version |
464
- | -------- | --------------------------------------------------- | ------- |
465
- | iOS | `ffmpeg-kit-ios-https-alt` | 6.0 |
466
- | Android | `io.github.jamaismagic.ffmpeg:ffmpeg-kit-main-16kb` | 6.1.4 |
596
+ The JavaScript, TypeScript, Swift, Kotlin, C core, and generated Nitro bridge in this repository are Apache-2.0.
467
597
 
468
- The bundled Android 6.1.4 artifact is GPL-enabled and includes x264 and x265. Distributing an Android application with this dependency can trigger GPLv3 source, license, and redistribution obligations. Its published Maven metadata does not fully communicate that posture, so assess the binaries and their notices rather than relying only on the POM license field.
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.
469
599
 
470
- FFmpeg's effective license depends on the enabled libraries, codecs, and build configuration. Before distributing an application:
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.
471
601
 
472
- 1. Review the license and notices shipped by each native dependency.
473
- 2. Identify the codecs and linked libraries used by your product.
474
- 3. Follow the applicable LGPL, GPL, attribution, relinking, and source-offer requirements.
475
- 4. Treat the current Android build as GPL-enabled, and reassess licensing again if you replace either native dependency.
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.
476
603
 
477
- 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.
478
605
 
479
606
  ## ๐Ÿ” Troubleshooting
480
607
 
@@ -496,7 +623,7 @@ Rebuild the native app after installing both `munim-ffmpeg` and `react-native-ni
496
623
 
497
624
  ### A codec or filter is missing
498
625
 
499
- Native FFmpeg variants do not bundle every codec, filter, or third-party library. Check `getFFmpegVersion()` and the session output, then choose a bundled codec or replace the native dependency with a build whose licensing and features fit your application.
626
+ 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).
500
627
 
501
628
  ### The Promise resolved but the command failed
502
629
 
@@ -504,24 +631,30 @@ Inspect `result.success`, `result.cancelled`, `result.returnCode`, `result.outpu
504
631
 
505
632
  ### iOS pod or build errors
506
633
 
507
- 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 used by Nitro's Swift/C++ bridge.
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`.
508
635
 
509
636
  ### Android build errors
510
637
 
511
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.
512
639
 
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`.
643
+
513
644
  ## Development
514
645
 
515
646
  ```bash
516
647
  npm install
517
- npm run codegen
518
- npm run typecheck
519
- npm run typecheck:example
520
- npm run build
521
- npm run pack:dry-run
648
+ npm run check # codegen, typecheck, example typecheck, build, pack dry-run
522
649
  ```
523
650
 
524
- Run the Expo example with:
651
+ Individual steps are available as `npm run codegen`, `typecheck`, `typecheck:example`, and `build`.
652
+
653
+ Nitrogen output under `nitrogen/generated` is committed. Change the `.nitro.ts` specification and rerun `npm run codegen` instead of editing generated files directly.
654
+
655
+ ### Example app
656
+
657
+ `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`.
525
658
 
526
659
  ```bash
527
660
  npm run example:ios
@@ -529,15 +662,27 @@ npm run example:ios
529
662
  npm run example:android
530
663
  ```
531
664
 
532
- Releases are validated and published manually; this repository does not use GitHub Actions:
665
+ FFmpeg encoding is slow in a simulator or emulator; run the suite on a physical device.
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
+
676
+ ### Releasing
677
+
678
+ Releases run locally from a clean `main`; this repository does not use GitHub Actions.
533
679
 
534
680
  ```bash
535
681
  npm run check
536
- cd packages/munim-ffmpeg
537
- npm publish --access public
682
+ npm run release:local
538
683
  ```
539
684
 
540
- Nitrogen output under `packages/munim-ffmpeg/nitrogen/generated` is committed. Change the `.nitro.ts` specification and rerun codegen instead of editing generated files directly.
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.
541
686
 
542
687
  ## ๐Ÿ‘ Contributing
543
688
 
@@ -139,6 +139,6 @@ dependencies {
139
139
  // Add a dependency on NitroModules
140
140
  implementation project(":react-native-nitro-modules")
141
141
 
142
- // Maintained FFmpegKit-compatible Android artifact with 16 KB page support.
143
- implementation "io.github.jamaismagic.ffmpeg:ffmpeg-kit-main-16kb:6.1.4"
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.
144
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
+ }