munim-ffmpeg 0.5.0 โ 0.6.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/README.md +224 -62
- package/android/src/main/java/com/margelo/nitro/munimffmpeg/FFmpegNative.kt +69 -67
- package/android/src/main/java/com/margelo/nitro/munimffmpeg/HybridMunimFfmpeg.kt +13 -33
- package/ios/HybridMunimFfmpeg.swift +65 -41
- package/ios/munim_ffmpeg_core.h +14 -1
- package/lib/index.js +3 -3
- package/package.json +1 -1
- package/scripts/binaries.json +1 -1
- package/scripts/fetch-binaries.mjs +11 -3
- package/src/index.ts +3 -3
package/README.md
CHANGED
|
@@ -114,26 +114,31 @@
|
|
|
114
114
|
- ๐ **Expo compatible:** Autolinking, config plugin, and an Expo development example
|
|
115
115
|
- ๐งช **Capability discovery:** Ask the bundled build which encoders, decoders, muxers, demuxers, filters, and protocols it actually has
|
|
116
116
|
- ๐ฌ **Subtitle burn-in:** libass renders ASS/SSA and SRT subtitles โ styling, positioning, outlines, shadows, and proper Arabic/Urdu shaping via HarfBuzz and FriBidi
|
|
117
|
+
- ๐ **Soft subtitle embedding:** Mux SRT/ASS tracks into MKV or MP4 so players can toggle them without re-encoding the video
|
|
118
|
+
- ๐ผ๏ธ **AVIF and AV1:** libaom encodes AV1 video and AVIF stills, dav1d decodes them
|
|
119
|
+
- ๐ฆ **One native library per platform:** a single `libmunimffmpeg.so` per Android ABI and one static library in the iOS xcframework, so nothing else has to be linked, loaded, or packaged
|
|
117
120
|
- ๐ฏ **TypeScript:** Complete public callback and result types
|
|
118
121
|
- ๐๏ธ **16 KB Android pages:** Built with the alignment Google Play requires
|
|
119
122
|
|
|
120
123
|
## Platform support matrix
|
|
121
124
|
|
|
122
|
-
| Capability | iOS
|
|
123
|
-
| ---------------------------- |
|
|
124
|
-
| FFmpeg argument execution | โ
|
|
125
|
-
| FFprobe argument execution | โ
|
|
126
|
-
| Parsed media information | โ
|
|
127
|
-
| Log callback | โ
|
|
128
|
-
| Encoding-statistics callback | โ
|
|
129
|
-
| Immediate session ID | โ
|
|
130
|
-
| Cancel one FFmpeg session | โ
|
|
131
|
-
| Cancel all FFmpeg sessions | โ
|
|
132
|
-
| Expo Go | โ
|
|
133
|
-
| Capability discovery | โ
|
|
134
|
-
| Subtitle burn-in | โ
|
|
135
|
-
| H.264 encoding | VideoToolbox
|
|
136
|
-
| Remote HTTP(S) inputs | โ
|
|
125
|
+
| Capability | iOS | Android | Notes |
|
|
126
|
+
| ---------------------------- | ------------ | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
127
|
+
| FFmpeg argument execution | โ
| โ
| Commands run asynchronously through the native compatibility library. |
|
|
128
|
+
| FFprobe argument execution | โ
| โ
| Custom FFprobe arguments return `FFmpegSessionResult`. |
|
|
129
|
+
| Parsed media information | โ
| โ
| `getMediaInformation()` returns parsed FFprobe JSON. |
|
|
130
|
+
| Log callback | โ
| โ
| Logs are delivered while a session is active. |
|
|
131
|
+
| Encoding-statistics callback | โ
| โ
| Available for FFmpeg execution. |
|
|
132
|
+
| Immediate session ID | โ
| โ
| `onSessionCreated` fires after the native session is created. |
|
|
133
|
+
| Cancel one FFmpeg session | โ
| โ
| Pass the positive safe-integer ID received by `execute`'s `onSessionCreated`. The native dependency does not expose FFprobe cancellation. |
|
|
134
|
+
| Cancel all FFmpeg sessions | โ
| โ
| Use `cancelAll()` or call `cancel()` without an ID. |
|
|
135
|
+
| Expo Go | โ | โ | A native development build is required. |
|
|
136
|
+
| Capability discovery | โ
| โ
| `listEncoders()`, `listDecoders()`, `listMuxers()`, `listDemuxers()`, `listFilters()`, `listProtocols()`, and `pickEncoder()` report what the bundled build supports. |
|
|
137
|
+
| Subtitle burn-in | โ
| โ
| libass with system fonts: Core Text on iOS, fontconfig over `/system/fonts` on Android. |
|
|
138
|
+
| H.264 encoding | VideoToolbox | MediaCodec | Hardware on both, `libopenh264` as the software fallback; use `pickEncoder(['h264_videotoolbox', 'h264_mediacodec', 'libopenh264'])` instead of hard-coding an encoder. |
|
|
139
|
+
| Remote HTTP(S) inputs | โ
| โ
| iOS links SecureTransport, Android links mbedTLS. Remote server behaviour still varies; prefer local files for predictable app workflows. |
|
|
140
|
+
| Soft subtitle embedding | โ
| โ
| Mux SRT/ASS as toggleable tracks (MKV: `srt`/`ass`, MP4: `mov_text`). |
|
|
141
|
+
| AVIF / AV1 encoding | โ
| โ
| `libaom-av1` encodes (add `-still-picture 1 -f avif` for images); `libdav1d` decodes. |
|
|
137
142
|
|
|
138
143
|
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.
|
|
139
144
|
|
|
@@ -141,14 +146,14 @@ Codec availability is determined by the native FFmpeg builds described in [Bundl
|
|
|
141
146
|
|
|
142
147
|
Every release runs the example's 25-check device suite. For 0.4.x:
|
|
143
148
|
|
|
144
|
-
| Target
|
|
145
|
-
|
|
|
146
|
-
| iPad Air (M3), iOS 26
|
|
147
|
-
| iOS Simulator, arm64
|
|
148
|
-
| Galaxy A14 5G, arm64-v8a
|
|
149
|
-
| Android emulator, arm64
|
|
150
|
-
| Android emulator, `x86_64` | Same: `libopenh264` passes, MediaCodec does not
|
|
151
|
-
| Android `armeabi-v7a`
|
|
149
|
+
| Target | Result |
|
|
150
|
+
| -------------------------- | ---------------------------------------------------------------- |
|
|
151
|
+
| iPad Air (M3), iOS 26 | 25/25 |
|
|
152
|
+
| iOS Simulator, arm64 | 25/25 |
|
|
153
|
+
| Galaxy A14 5G, arm64-v8a | 25/25 |
|
|
154
|
+
| Android emulator, arm64 | Software encoding passes; hardware encoding does not โ see below |
|
|
155
|
+
| Android emulator, `x86_64` | Same: `libopenh264` passes, MediaCodec does not |
|
|
156
|
+
| Android `armeabi-v7a` | Built and statically checked, not executed |
|
|
152
157
|
|
|
153
158
|
`x86_64` was verified on an Intel Windows machine, since the Android emulator refuses non-native system images on Apple Silicon: FFmpeg 9.0.1, 185 encoders, and the software H.264 encode passes. `armeabi-v7a` has no hardware to hand, so it was checked statically instead โ correct ELF architecture, the expected JNI exports, only system libraries unresolved, and the same FFmpeg and codec set as arm64.
|
|
154
159
|
|
|
@@ -158,11 +163,20 @@ An Android emulator has no working MediaCodec **encoder**. `h264_mediacodec` and
|
|
|
158
163
|
|
|
159
164
|
This is an emulator limitation, not a package one, but it is worth knowing before debugging: **test video encoding on a physical device**.
|
|
160
165
|
|
|
161
|
-
`pickEncoder` cannot detect it, because MediaCodec
|
|
166
|
+
`pickEncoder` cannot detect it, because MediaCodec _is_ present in an emulator โ it just does not work. When you need encoding to succeed regardless of environment, ask for the software encoder by name:
|
|
162
167
|
|
|
163
168
|
```typescript
|
|
164
169
|
// Deterministic anywhere: emulators, CI, older devices.
|
|
165
|
-
await execute([
|
|
170
|
+
await execute([
|
|
171
|
+
'-y',
|
|
172
|
+
'-i',
|
|
173
|
+
input,
|
|
174
|
+
'-c:v',
|
|
175
|
+
'libopenh264',
|
|
176
|
+
'-pix_fmt',
|
|
177
|
+
'yuv420p',
|
|
178
|
+
output,
|
|
179
|
+
])
|
|
166
180
|
```
|
|
167
181
|
|
|
168
182
|
`libopenh264`, `mpeg4` and `libvpx-vp9` are all software encoders and work everywhere.
|
|
@@ -171,30 +185,34 @@ await execute(['-y', '-i', input, '-c:v', 'libopenh264', '-pix_fmt', 'yuv420p',
|
|
|
171
185
|
|
|
172
186
|
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.
|
|
173
187
|
|
|
174
|
-
|
|
|
175
|
-
|
|
|
176
|
-
| FFmpeg
|
|
177
|
-
| Architectures
|
|
178
|
-
| Hardware codecs | VideoToolbox, AudioToolbox
|
|
179
|
-
| TLS
|
|
180
|
-
| Minimum
|
|
188
|
+
| | iOS | Android |
|
|
189
|
+
| --------------- | ------------------------------------------------------- | ------------------------------- |
|
|
190
|
+
| FFmpeg | 9.0.1 | 9.0.1 |
|
|
191
|
+
| Architectures | arm64 device, arm64 + x86_64 simulator | arm64-v8a, armeabi-v7a, x86_64 |
|
|
192
|
+
| Hardware codecs | VideoToolbox, AudioToolbox | MediaCodec |
|
|
193
|
+
| TLS | SecureTransport | mbedTLS |
|
|
194
|
+
| Minimum | iOS 15.1 | API 24, 16 KB pages |
|
|
195
|
+
| Ships as | `MunimFFmpeg.xcframework`, one static library per slice | one `libmunimffmpeg.so` per ABI |
|
|
181
196
|
|
|
182
|
-
Linked libraries, identical on both: **LAME** (MP3), **Opus**, **libvpx** (VP8/VP9), **dav1d** (AV1 decoding), **openh264** (software H.264), **libass** with **FreeType**, **HarfBuzz**, and **FriBidi** (subtitle rendering and text shaping), plus everything FFmpeg builds natively. Android additionally links **fontconfig** and **expat** so libass can discover the system fonts; iOS uses Core Text for the same job.
|
|
197
|
+
Linked libraries, identical on both: **LAME** (MP3), **Opus**, **libvpx** (VP8/VP9), **dav1d** (AV1 decoding), **libaom** (AV1 and AVIF encoding), **openh264** (software H.264), **libass** with **FreeType**, **HarfBuzz**, and **FriBidi** (subtitle rendering and text shaping), plus everything FFmpeg builds natively. Android additionally links **fontconfig** and **expat** so libass can discover the system fonts; iOS uses Core Text for the same job.
|
|
183
198
|
|
|
184
199
|
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.
|
|
185
200
|
|
|
201
|
+
Everything above is linked statically into a single library per platform: FFmpeg, both tools, the core, and every external library. On Android that is one `libmunimffmpeg.so` per ABI whose only dependencies are the system libraries and the `libc++_shared.so` React Native already bundles; on iOS it is one static library per slice inside `MunimFFmpeg.xcframework`. Only the JNI entry points are exported on Android, so the bundled FFmpeg cannot collide with another copy an app might carry.
|
|
202
|
+
|
|
186
203
|
### Encoders
|
|
187
204
|
|
|
188
205
|
Verified by running the example's device suite: iOS reports 187 encoders, Android 185. Everything FFmpeg builds natively (`aac`, `alac`, `flac`, `mpeg4`, `mjpeg`, `png`, `gif`, `pcm_*`, โฆ) is on both, as are `libmp3lame`, `libopus`, `libvpx`, and `libvpx-vp9`.
|
|
189
206
|
|
|
190
207
|
H.264 and HEVC come from the platform's hardware encoder, which is faster and uses less power than a software encoder. `libopenh264` is there as a software H.264 fallback for anywhere hardware encoding is unavailable โ an emulator, for instance:
|
|
191
208
|
|
|
192
|
-
| Encoder
|
|
193
|
-
|
|
|
194
|
-
| `h264_videotoolbox`, `hevc_videotoolbox`, `prores_videotoolbox`
|
|
195
|
-
| `h264_mediacodec`, `hevc_mediacodec`, `vp8_mediacodec`, `vp9_mediacodec` | โ
|
|
196
|
-
| `aac_at`, `alac_at` (AudioToolbox)
|
|
197
|
-
| `libopenh264` (H.264, software)
|
|
209
|
+
| Encoder | iOS | Android |
|
|
210
|
+
| ------------------------------------------------------------------------ | --- | ------- |
|
|
211
|
+
| `h264_videotoolbox`, `hevc_videotoolbox`, `prores_videotoolbox` | โ
| โ |
|
|
212
|
+
| `h264_mediacodec`, `hevc_mediacodec`, `vp8_mediacodec`, `vp9_mediacodec` | โ | โ
|
|
|
213
|
+
| `aac_at`, `alac_at` (AudioToolbox) | โ
| โ |
|
|
214
|
+
| `libopenh264` (H.264, software) | โ
| โ
|
|
|
215
|
+
| `libaom-av1` (AV1, software; AVIF stills) | โ
| โ
|
|
|
198
216
|
|
|
199
217
|
Resolve the name at runtime instead of branching on `Platform.OS`:
|
|
200
218
|
|
|
@@ -214,7 +232,7 @@ await execute(['-y', '-i', inputPath, '-c:v', h264, outputPath])
|
|
|
214
232
|
|
|
215
233
|
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.
|
|
216
234
|
|
|
217
|
-
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.
|
|
235
|
+
Decoding is uniform: H.264, HEVC, VP8/VP9, AV1 (via dav1d, including AVIF images), MPEG-4, MP3, AAC, Vorbis, Opus, FLAC and the usual containers, on both platforms. Both link TLS, so `https://` inputs work.
|
|
218
236
|
|
|
219
237
|
## ๐ฆ Installation
|
|
220
238
|
|
|
@@ -279,7 +297,7 @@ You can also create an [EAS development build](https://docs.expo.dev/develop/dev
|
|
|
279
297
|
|
|
280
298
|
### Native binaries
|
|
281
299
|
|
|
282
|
-
The FFmpeg libraries are
|
|
300
|
+
The FFmpeg libraries are well over 100 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`.
|
|
283
301
|
|
|
284
302
|
If your environment blocks install scripts (`npm install --ignore-scripts`), fetch them explicitly:
|
|
285
303
|
|
|
@@ -567,11 +585,16 @@ if (!result.success) {
|
|
|
567
585
|
```typescript
|
|
568
586
|
import { execute, pickEncoder } from 'munim-ffmpeg'
|
|
569
587
|
|
|
570
|
-
const encoder = await pickEncoder([
|
|
588
|
+
const encoder = await pickEncoder([
|
|
589
|
+
'h264_videotoolbox',
|
|
590
|
+
'h264_mediacodec',
|
|
591
|
+
'libopenh264',
|
|
592
|
+
])
|
|
571
593
|
if (!encoder) throw new Error('No H.264 encoder available in this build')
|
|
572
594
|
|
|
573
|
-
//
|
|
574
|
-
const
|
|
595
|
+
// MediaCodec wants NV12 input; the others take planar YUV.
|
|
596
|
+
const pixelFormat = encoder === 'h264_mediacodec' ? 'nv12' : 'yuv420p'
|
|
597
|
+
const quality = ['-b:v', '2M', '-pix_fmt', pixelFormat]
|
|
575
598
|
|
|
576
599
|
const result = await execute([
|
|
577
600
|
'-y',
|
|
@@ -607,6 +630,39 @@ const result = await execute([
|
|
|
607
630
|
])
|
|
608
631
|
```
|
|
609
632
|
|
|
633
|
+
### Write an AVIF still
|
|
634
|
+
|
|
635
|
+
AVIF is AV1 in an image container. `libaom-av1` encodes it; `-still-picture 1` switches the encoder into single-image mode and `-f avif` picks the container. Decoding an AVIF back โ or any AV1 video โ goes through dav1d automatically.
|
|
636
|
+
|
|
637
|
+
```typescript
|
|
638
|
+
await execute([
|
|
639
|
+
'-y',
|
|
640
|
+
'-ss',
|
|
641
|
+
'1.5',
|
|
642
|
+
'-i',
|
|
643
|
+
inputPath,
|
|
644
|
+
'-frames:v',
|
|
645
|
+
'1',
|
|
646
|
+
'-vf',
|
|
647
|
+
'scale=-2:720',
|
|
648
|
+
'-c:v',
|
|
649
|
+
'libaom-av1',
|
|
650
|
+
'-still-picture',
|
|
651
|
+
'1',
|
|
652
|
+
'-cpu-used',
|
|
653
|
+
'6', // 0 (slowest, best) โฆ 8 (fastest)
|
|
654
|
+
'-crf',
|
|
655
|
+
'28', // quality; lower is larger
|
|
656
|
+
'-pix_fmt',
|
|
657
|
+
'yuv420p',
|
|
658
|
+
'-f',
|
|
659
|
+
'avif',
|
|
660
|
+
outputPath, // ends in .avif
|
|
661
|
+
])
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
React Native's `<Image>` displays AVIF natively on iOS 16+ and Android 12+.
|
|
665
|
+
|
|
610
666
|
### Burn subtitles into a video
|
|
611
667
|
|
|
612
668
|
The bundled builds include libass with FreeType, HarfBuzz, and FriBidi, so ASS/SSA styling and complex scripts (Arabic, Urdu, and other RTL or shaped text) render correctly. System fonts are found automatically โ through Core Text on iOS and through fontconfig scanning `/system/fonts` on Android.
|
|
@@ -618,18 +674,24 @@ import { execute, normalizePath } from 'munim-ffmpeg'
|
|
|
618
674
|
// shadows, positioning, karaoke โ everything the format supports.
|
|
619
675
|
await execute([
|
|
620
676
|
'-y',
|
|
621
|
-
'-i',
|
|
622
|
-
|
|
623
|
-
'-
|
|
677
|
+
'-i',
|
|
678
|
+
inputPath,
|
|
679
|
+
'-vf',
|
|
680
|
+
`ass=filename=${normalizePath(subtitlePath)}`,
|
|
681
|
+
'-c:a',
|
|
682
|
+
'copy',
|
|
624
683
|
outputPath,
|
|
625
684
|
])
|
|
626
685
|
|
|
627
686
|
// SRT can be styled at burn time with force_style.
|
|
628
687
|
await execute([
|
|
629
688
|
'-y',
|
|
630
|
-
'-i',
|
|
631
|
-
|
|
632
|
-
'-
|
|
689
|
+
'-i',
|
|
690
|
+
inputPath,
|
|
691
|
+
'-vf',
|
|
692
|
+
`subtitles=filename=${normalizePath(srtPath)}:force_style='Fontsize=28,PrimaryColour=&H00FFFF00,Outline=2'`,
|
|
693
|
+
'-c:a',
|
|
694
|
+
'copy',
|
|
633
695
|
outputPath,
|
|
634
696
|
])
|
|
635
697
|
```
|
|
@@ -646,19 +708,114 @@ import { execute } from 'munim-ffmpeg'
|
|
|
646
708
|
// Bundle one video, two audio languages, and a subtitle track into MKV.
|
|
647
709
|
await execute([
|
|
648
710
|
'-y',
|
|
649
|
-
'-i',
|
|
650
|
-
|
|
651
|
-
'-
|
|
652
|
-
|
|
711
|
+
'-i',
|
|
712
|
+
videoPath,
|
|
713
|
+
'-i',
|
|
714
|
+
urduAudioPath,
|
|
715
|
+
'-i',
|
|
716
|
+
subtitlePath,
|
|
717
|
+
'-map',
|
|
718
|
+
'0:v:0',
|
|
719
|
+
'-map',
|
|
720
|
+
'0:a:0',
|
|
721
|
+
'-map',
|
|
722
|
+
'1:a:0',
|
|
723
|
+
'-map',
|
|
724
|
+
'2:s:0',
|
|
725
|
+
'-c:v',
|
|
726
|
+
'copy',
|
|
727
|
+
'-c:a',
|
|
728
|
+
'aac',
|
|
729
|
+
'-c:s',
|
|
730
|
+
'srt',
|
|
731
|
+
'-metadata:s:a:1',
|
|
732
|
+
'language=urd',
|
|
653
733
|
outputMkvPath,
|
|
654
734
|
])
|
|
655
735
|
|
|
656
736
|
// Extract the second audio track without re-encoding.
|
|
657
737
|
await execute([
|
|
658
|
-
'-y',
|
|
738
|
+
'-y',
|
|
739
|
+
'-i',
|
|
740
|
+
outputMkvPath,
|
|
741
|
+
'-map',
|
|
742
|
+
'0:a:1',
|
|
743
|
+
'-c',
|
|
744
|
+
'copy',
|
|
745
|
+
trackPath,
|
|
746
|
+
])
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
### Embed soft subtitles
|
|
750
|
+
|
|
751
|
+
The other subtitle workflow: instead of burning text into the frames, mux the subtitle file in as its own stream, so players can toggle it and the video is never re-encoded. Each container wants its own subtitle codec โ MKV takes `srt` and `ass` (ASS keeps its styling), MP4 takes `mov_text`, WebM takes `webvtt`.
|
|
752
|
+
|
|
753
|
+
```typescript
|
|
754
|
+
import { execute } from 'munim-ffmpeg'
|
|
755
|
+
|
|
756
|
+
// MKV with English SRT and styled Urdu ASS tracks, video and audio untouched.
|
|
757
|
+
await execute([
|
|
758
|
+
'-y',
|
|
759
|
+
'-i',
|
|
760
|
+
videoPath,
|
|
761
|
+
'-i',
|
|
762
|
+
englishSrtPath,
|
|
763
|
+
'-i',
|
|
764
|
+
urduAssPath,
|
|
765
|
+
'-map',
|
|
766
|
+
'0:v:0',
|
|
767
|
+
'-map',
|
|
768
|
+
'0:a:0',
|
|
769
|
+
'-map',
|
|
770
|
+
'1:0',
|
|
771
|
+
'-map',
|
|
772
|
+
'2:0',
|
|
773
|
+
'-c:v',
|
|
774
|
+
'copy',
|
|
775
|
+
'-c:a',
|
|
776
|
+
'copy',
|
|
777
|
+
'-c:s:0',
|
|
778
|
+
'srt',
|
|
779
|
+
'-c:s:1',
|
|
780
|
+
'ass',
|
|
781
|
+
'-metadata:s:s:0',
|
|
782
|
+
'language=eng',
|
|
783
|
+
'-metadata:s:s:0',
|
|
784
|
+
'title=English',
|
|
785
|
+
'-metadata:s:s:1',
|
|
786
|
+
'language=urd',
|
|
787
|
+
'-metadata:s:s:1',
|
|
788
|
+
'title=Urdu',
|
|
789
|
+
outputMkvPath,
|
|
790
|
+
])
|
|
791
|
+
|
|
792
|
+
// MP4 needs mov_text instead.
|
|
793
|
+
await execute([
|
|
794
|
+
'-y',
|
|
795
|
+
'-i',
|
|
796
|
+
videoPath,
|
|
797
|
+
'-i',
|
|
798
|
+
subtitlePath,
|
|
799
|
+
'-map',
|
|
800
|
+
'0:v:0',
|
|
801
|
+
'-map',
|
|
802
|
+
'0:a:0',
|
|
803
|
+
'-map',
|
|
804
|
+
'1:0',
|
|
805
|
+
'-c:v',
|
|
806
|
+
'copy',
|
|
807
|
+
'-c:a',
|
|
808
|
+
'copy',
|
|
809
|
+
'-c:s',
|
|
810
|
+
'mov_text',
|
|
811
|
+
'-metadata:s:s:0',
|
|
812
|
+
'language=eng',
|
|
813
|
+
outputMp4Path,
|
|
659
814
|
])
|
|
660
815
|
```
|
|
661
816
|
|
|
817
|
+
Existing subtitle streams survive remuxing with `-map 0 -c copy`, and a soft track can later be burned in with `-vf subtitles=filename=input.mkv:si=0` (the `si` option picks the subtitle stream index).
|
|
818
|
+
|
|
662
819
|
### Cancel a long-running command
|
|
663
820
|
|
|
664
821
|
```typescript
|
|
@@ -711,9 +868,9 @@ if (result.success) {
|
|
|
711
868
|
|
|
712
869
|
The JavaScript, TypeScript, Swift, Kotlin, C core, and generated Nitro bridge in this repository are Apache-2.0.
|
|
713
870
|
|
|
714
|
-
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. The external libraries it links are LAME (LGPL), Opus (BSD), libvpx (BSD), dav1d (BSD), openh264 (BSD 2-clause), libass (ISC), FreeType (FTL, BSD-style with credit), HarfBuzz (MIT-style), FriBidi (LGPL), and, on Android only, mbedTLS (Apache-2.0), fontconfig (MIT-style), and expat (MIT). None of them change the LGPL story.
|
|
871
|
+
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. The external libraries it links are LAME (LGPL), Opus (BSD), libvpx (BSD), dav1d (BSD), libaom (BSD 2-clause with the Alliance for Open Media patent licence), openh264 (BSD 2-clause), libass (ISC), FreeType (FTL, BSD-style with credit), HarfBuzz (MIT-style), FriBidi (LGPL), and, on Android only, mbedTLS (Apache-2.0), fontconfig (MIT-style), and expat (MIT). None of them change the LGPL story.
|
|
715
872
|
|
|
716
|
-
> **A note on H.264 patents.** Hardware encoders are covered by the licences device manufacturers already pay for. Software H.264 encoding through `libopenh264` is not: Cisco's royalty coverage applies to
|
|
873
|
+
> **A note on H.264 patents.** Hardware encoders are covered by the licences device manufacturers already pay for. Software H.264 encoding through `libopenh264` is not: Cisco's royalty coverage applies to _their_ prebuilt binary, and this package builds openh264 from source. If you ship software H.264 encoding at scale, check where you stand with AVC licensing. Hardware encoders avoid the question entirely, which is why `pickEncoder` should list them first.
|
|
717
874
|
|
|
718
875
|
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.
|
|
719
876
|
|
|
@@ -741,7 +898,7 @@ Rebuild the native app after installing both `munim-ffmpeg` and `react-native-ni
|
|
|
741
898
|
|
|
742
899
|
### A codec or filter is missing
|
|
743
900
|
|
|
744
|
-
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 `
|
|
901
|
+
Native FFmpeg variants do not bundle every codec, filter, or third-party library, and the iOS and Android builds are not identical. Call `listEncoders()`, `listDecoders()`, `listMuxers()`, `listDemuxers()`, `listFilters()`, or `listProtocols()` to see what the running build actually has, and prefer `pickEncoder()` over a hard-coded name. There is no `libx264` in these LGPL builds โ H.264 comes from the platform's hardware encoder or `libopenh264` โ see [Bundled FFmpeg builds](#bundled-ffmpeg-builds).
|
|
745
902
|
|
|
746
903
|
### The Promise resolved but the command failed
|
|
747
904
|
|
|
@@ -772,7 +929,10 @@ Nitrogen output under `nitrogen/generated` is committed. Change the `.nitro.ts`
|
|
|
772
929
|
|
|
773
930
|
### Example app
|
|
774
931
|
|
|
775
|
-
`example/` is an Expo app
|
|
932
|
+
`example/` is an Expo SDK 57 app โ a development build, since Expo Go cannot load native modules โ with two screens:
|
|
933
|
+
|
|
934
|
+
- **Playground** picks a video with `expo-document-picker` (or generates one from JavaScript fixtures), inspects it, transcodes it through the device's hardware H.264 encoder via `pickEncoder`, writes an AVIF still, embeds soft subtitles into an MKV, burns them in with libass, and shows progress from the statistics callback with a cancel button wired to `onSessionCreated`. Each action is a plain argument array, so [`example/Playground.tsx`](./example/Playground.tsx) doubles as a recipe book.
|
|
935
|
+
- **Device suite** runs the 30+ checks used to verify every release: H.264 and HEVC encoding, VP9/Opus in WebM, MP3, AAC, AVIF, scaling and multi-step filter graphs, software H.264 via openh264, subtitle burn-in and embedding, muxing, demuxing, trimming, concatenation, thumbnails, audio resampling, awkward file paths, concurrent sessions, single and global cancellation, protocol support, and both failure paths. It runs on launch, renders each result, writes `munim-ffmpeg-suite.json` to the app's document directory, and logs it as `MUNIM_FFMPEG_SUITE_RESULT`.
|
|
776
936
|
|
|
777
937
|
```bash
|
|
778
938
|
npm run example:ios
|
|
@@ -780,7 +940,7 @@ npm run example:ios
|
|
|
780
940
|
npm run example:android
|
|
781
941
|
```
|
|
782
942
|
|
|
783
|
-
FFmpeg encoding is slow in a simulator or emulator; run
|
|
943
|
+
FFmpeg encoding is slow in a simulator or emulator, and emulators have no working hardware encoder; run it on a physical device. [`example/README.md`](./example/README.md) has the details.
|
|
784
944
|
|
|
785
945
|
### Rebuilding FFmpeg
|
|
786
946
|
|
|
@@ -793,14 +953,16 @@ See [`scripts/ffmpeg/README.md`](./scripts/ffmpeg/README.md) for what the build
|
|
|
793
953
|
|
|
794
954
|
### Releasing
|
|
795
955
|
|
|
796
|
-
Releases run locally from a clean `main
|
|
956
|
+
Releases run locally from a clean `main`:
|
|
797
957
|
|
|
798
958
|
```bash
|
|
799
959
|
npm run check
|
|
800
960
|
npm run release:local
|
|
801
961
|
```
|
|
802
962
|
|
|
803
|
-
`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
|
|
963
|
+
`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` and `build-info.txt` to the GitHub release, which is where `postinstall` fetches the binaries from โ so run `npm run binaries:package` first.
|
|
964
|
+
|
|
965
|
+
Two GitHub Actions workflows back this up: `CI` checks the JavaScript surface on every push and pull request, and `Build binaries` compiles every iOS and Android slice in parallel, on demand or when a pull request touches `scripts/ffmpeg/`, and can attach the result to a release. See [`scripts/ffmpeg/README.md`](./scripts/ffmpeg/README.md#building-in-github-actions).
|
|
804
966
|
|
|
805
967
|
## ๐ Contributing
|
|
806
968
|
|
|
@@ -5,6 +5,55 @@ import androidx.annotation.Keep
|
|
|
5
5
|
import com.facebook.proguard.annotations.DoNotStrip
|
|
6
6
|
import java.io.File
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Per-run callback target. The JNI bridge holds a global reference to this
|
|
10
|
+
* object for the duration of its execution and the native core only routes
|
|
11
|
+
* callbacks to whichever session actually holds the execution lock, so
|
|
12
|
+
* concurrently submitted sessions never see each other's logs.
|
|
13
|
+
*/
|
|
14
|
+
@Keep
|
|
15
|
+
@DoNotStrip
|
|
16
|
+
class FFmpegSession(
|
|
17
|
+
private val logSink: ((String) -> Unit)?,
|
|
18
|
+
private val statisticsSink: ((
|
|
19
|
+
timeMs: Double,
|
|
20
|
+
sizeBytes: Double,
|
|
21
|
+
bitrateKbits: Double,
|
|
22
|
+
speed: Double,
|
|
23
|
+
videoFrameNumber: Double,
|
|
24
|
+
fps: Double,
|
|
25
|
+
quality: Double,
|
|
26
|
+
) -> Unit)?,
|
|
27
|
+
) {
|
|
28
|
+
private val buffer = StringBuilder()
|
|
29
|
+
|
|
30
|
+
val output: String
|
|
31
|
+
get() = synchronized(buffer) { buffer.toString() }
|
|
32
|
+
|
|
33
|
+
// FFmpeg logs from several of its own threads at once, so the buffer needs
|
|
34
|
+
// the lock even though each session belongs to a single execution.
|
|
35
|
+
@Keep
|
|
36
|
+
@DoNotStrip
|
|
37
|
+
fun onLog(message: String) {
|
|
38
|
+
synchronized(buffer) { buffer.append(message) }
|
|
39
|
+
logSink?.invoke(message)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@Keep
|
|
43
|
+
@DoNotStrip
|
|
44
|
+
fun onStatistics(
|
|
45
|
+
timeMs: Double,
|
|
46
|
+
sizeBytes: Double,
|
|
47
|
+
bitrateKbits: Double,
|
|
48
|
+
speed: Double,
|
|
49
|
+
videoFrameNumber: Double,
|
|
50
|
+
fps: Double,
|
|
51
|
+
quality: Double,
|
|
52
|
+
) {
|
|
53
|
+
statisticsSink?.invoke(timeMs, sizeBytes, bitrateKbits, speed, videoFrameNumber, fps, quality)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
8
57
|
/**
|
|
9
58
|
* Thin wrapper over FFmpeg 9's own command-line tools, compiled to run inside
|
|
10
59
|
* the app process.
|
|
@@ -18,9 +67,28 @@ import java.io.File
|
|
|
18
67
|
object FFmpegNative {
|
|
19
68
|
init {
|
|
20
69
|
configureFontconfig()
|
|
21
|
-
System.loadLibrary("
|
|
70
|
+
System.loadLibrary("munimffmpeg")
|
|
22
71
|
}
|
|
23
72
|
|
|
73
|
+
/** Return code the tools report when a run was cancelled. */
|
|
74
|
+
const val CANCELLED = 255
|
|
75
|
+
|
|
76
|
+
external fun nativeVersion(): String
|
|
77
|
+
|
|
78
|
+
external fun nativeExecute(
|
|
79
|
+
arguments: Array<String>,
|
|
80
|
+
stdoutPath: String,
|
|
81
|
+
session: FFmpegSession,
|
|
82
|
+
): Int
|
|
83
|
+
|
|
84
|
+
external fun nativeExecuteProbe(
|
|
85
|
+
arguments: Array<String>,
|
|
86
|
+
outputPath: String,
|
|
87
|
+
session: FFmpegSession,
|
|
88
|
+
): Int
|
|
89
|
+
|
|
90
|
+
external fun nativeCancel()
|
|
91
|
+
|
|
24
92
|
/**
|
|
25
93
|
* libass discovers fonts through fontconfig, and Android has no fonts.conf,
|
|
26
94
|
* so one pointing at the system font directories is written to the app's
|
|
@@ -53,70 +121,4 @@ object FFmpegNative {
|
|
|
53
121
|
Os.setenv("FONTCONFIG_FILE", configuration.absolutePath, true)
|
|
54
122
|
}
|
|
55
123
|
}
|
|
56
|
-
|
|
57
|
-
/** Return code the tools report when a run was cancelled. */
|
|
58
|
-
const val CANCELLED = 255
|
|
59
|
-
|
|
60
|
-
external fun nativeVersion(): String
|
|
61
|
-
|
|
62
|
-
external fun nativeExecute(arguments: Array<String>, stdoutPath: String): Int
|
|
63
|
-
|
|
64
|
-
external fun nativeExecuteProbe(arguments: Array<String>, outputPath: String): Int
|
|
65
|
-
|
|
66
|
-
external fun nativeCancel()
|
|
67
|
-
|
|
68
|
-
data class Statistics(
|
|
69
|
-
val timeMs: Double,
|
|
70
|
-
val sizeBytes: Double,
|
|
71
|
-
val bitrateKbits: Double,
|
|
72
|
-
val speed: Double,
|
|
73
|
-
val videoFrameNumber: Double,
|
|
74
|
-
val fps: Double,
|
|
75
|
-
val quality: Double,
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
@Volatile
|
|
79
|
-
private var logSink: ((String) -> Unit)? = null
|
|
80
|
-
|
|
81
|
-
@Volatile
|
|
82
|
-
private var statisticsSink: ((Statistics) -> Unit)? = null
|
|
83
|
-
|
|
84
|
-
fun <T> withCallbacks(
|
|
85
|
-
onLog: ((String) -> Unit)?,
|
|
86
|
-
onStatistics: ((Statistics) -> Unit)?,
|
|
87
|
-
body: () -> T,
|
|
88
|
-
): T {
|
|
89
|
-
logSink = onLog
|
|
90
|
-
statisticsSink = onStatistics
|
|
91
|
-
try {
|
|
92
|
-
return body()
|
|
93
|
-
} finally {
|
|
94
|
-
logSink = null
|
|
95
|
-
statisticsSink = null
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
@JvmStatic
|
|
100
|
-
@Keep
|
|
101
|
-
@DoNotStrip
|
|
102
|
-
fun onLog(message: String) {
|
|
103
|
-
logSink?.invoke(message)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
@JvmStatic
|
|
107
|
-
@Keep
|
|
108
|
-
@DoNotStrip
|
|
109
|
-
fun onStatistics(
|
|
110
|
-
timeMs: Double,
|
|
111
|
-
sizeBytes: Double,
|
|
112
|
-
bitrateKbits: Double,
|
|
113
|
-
speed: Double,
|
|
114
|
-
videoFrameNumber: Double,
|
|
115
|
-
fps: Double,
|
|
116
|
-
quality: Double,
|
|
117
|
-
) {
|
|
118
|
-
statisticsSink?.invoke(
|
|
119
|
-
Statistics(timeMs, sizeBytes, bitrateKbits, speed, videoFrameNumber, fps, quality)
|
|
120
|
-
)
|
|
121
|
-
}
|
|
122
124
|
}
|
|
@@ -53,37 +53,21 @@ class HybridMunimFfmpeg : HybridMunimFfmpegSpec() {
|
|
|
53
53
|
onSessionCreated?.invoke(sessionId)
|
|
54
54
|
|
|
55
55
|
return Promise.async {
|
|
56
|
-
val output = StringBuilder()
|
|
57
56
|
val startedAt = System.currentTimeMillis()
|
|
58
57
|
// ffmpeg prints reports such as -encoders and -protocols to stdout rather
|
|
59
58
|
// than through its logger, so it is captured to a file and appended.
|
|
60
59
|
val stdout = File.createTempFile("munim-ffmpeg", ".txt")
|
|
61
60
|
|
|
62
|
-
val
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
onStatistics = { statistics ->
|
|
68
|
-
onStatistics?.invoke(
|
|
69
|
-
statistics.timeMs,
|
|
70
|
-
statistics.sizeBytes,
|
|
71
|
-
statistics.bitrateKbits,
|
|
72
|
-
statistics.speed,
|
|
73
|
-
statistics.videoFrameNumber,
|
|
74
|
-
statistics.fps,
|
|
75
|
-
statistics.quality,
|
|
76
|
-
)
|
|
77
|
-
},
|
|
78
|
-
) {
|
|
79
|
-
FFmpegNative.nativeExecute(arguments_, stdout.absolutePath)
|
|
80
|
-
}
|
|
61
|
+
val session = FFmpegSession(
|
|
62
|
+
logSink = { message -> onLog?.invoke(message) },
|
|
63
|
+
statisticsSink = onStatistics,
|
|
64
|
+
)
|
|
65
|
+
val returnCode = FFmpegNative.nativeExecute(arguments_, stdout.absolutePath, session)
|
|
81
66
|
|
|
82
67
|
val printed = runCatching { stdout.readText() }.getOrDefault("")
|
|
83
68
|
stdout.delete()
|
|
84
|
-
output.append(printed)
|
|
85
69
|
|
|
86
|
-
result(sessionId, returnCode, output
|
|
70
|
+
result(sessionId, returnCode, session.output + printed, System.currentTimeMillis() - startedAt)
|
|
87
71
|
}
|
|
88
72
|
}
|
|
89
73
|
|
|
@@ -138,19 +122,15 @@ class HybridMunimFfmpeg : HybridMunimFfmpegSpec() {
|
|
|
138
122
|
): Pair<Int, String> {
|
|
139
123
|
val destination = File.createTempFile("munim-ffprobe", ".txt")
|
|
140
124
|
try {
|
|
141
|
-
val
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
onStatistics = null,
|
|
148
|
-
) {
|
|
149
|
-
FFmpegNative.nativeExecuteProbe(arguments, destination.absolutePath)
|
|
150
|
-
}
|
|
125
|
+
val session = FFmpegSession(
|
|
126
|
+
logSink = { message -> onLog?.invoke(message) },
|
|
127
|
+
statisticsSink = null,
|
|
128
|
+
)
|
|
129
|
+
val returnCode =
|
|
130
|
+
FFmpegNative.nativeExecuteProbe(arguments, destination.absolutePath, session)
|
|
151
131
|
|
|
152
132
|
val report = destination.readText()
|
|
153
|
-
return returnCode to report.ifEmpty {
|
|
133
|
+
return returnCode to report.ifEmpty { session.output }
|
|
154
134
|
} finally {
|
|
155
135
|
destination.delete()
|
|
156
136
|
}
|
|
@@ -1,21 +1,39 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
import NitroModules
|
|
3
3
|
|
|
4
|
-
///
|
|
5
|
-
///
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
4
|
+
/// Per-run callback target, handed to the core as the context pointer. The
|
|
5
|
+
/// core only routes callbacks to whichever session actually holds its
|
|
6
|
+
/// execution lock, so concurrently submitted sessions never see each other's
|
|
7
|
+
/// logs.
|
|
8
|
+
private final class Session {
|
|
9
|
+
let onLog: ((String) -> Void)?
|
|
10
|
+
let onStatistics: ((Double, Double, Double, Double, Double, Double, Double) -> Void)?
|
|
11
|
+
private var buffer = ""
|
|
12
|
+
// FFmpeg logs from several of its own threads at once, so the buffer needs
|
|
13
|
+
// the lock even though each session belongs to a single execution.
|
|
14
|
+
private let lock = NSLock()
|
|
15
|
+
|
|
16
|
+
init(
|
|
17
|
+
onLog: ((String) -> Void)?,
|
|
18
|
+
onStatistics: ((Double, Double, Double, Double, Double, Double, Double) -> Void)? = nil
|
|
19
|
+
) {
|
|
20
|
+
self.onLog = onLog
|
|
21
|
+
self.onStatistics = onStatistics
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func append(_ text: String) {
|
|
25
|
+
lock.lock()
|
|
26
|
+
buffer += text
|
|
27
|
+
lock.unlock()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
var output: String {
|
|
31
|
+
lock.lock()
|
|
32
|
+
defer { lock.unlock() }
|
|
33
|
+
return buffer
|
|
15
34
|
}
|
|
16
35
|
}
|
|
17
36
|
|
|
18
|
-
private let active = ActiveSession()
|
|
19
37
|
// Concurrent on purpose: the C core serialises executions behind its own lock,
|
|
20
38
|
// and a request that is waiting there can still be cancelled. A serial queue
|
|
21
39
|
// would hold the second call outside the core, where cancelAll() cannot see it.
|
|
@@ -25,16 +43,17 @@ private let executionQueue = DispatchQueue(
|
|
|
25
43
|
attributes: .concurrent
|
|
26
44
|
)
|
|
27
45
|
|
|
28
|
-
private
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
46
|
+
private let installCallbacks: Void = munim_ffmpeg_set_callbacks({ context, message in
|
|
47
|
+
guard let context, let message else { return }
|
|
48
|
+
let session = Unmanaged<Session>.fromOpaque(context).takeUnretainedValue()
|
|
49
|
+
let text = String(cString: message)
|
|
50
|
+
session.append(text)
|
|
51
|
+
session.onLog?(text)
|
|
52
|
+
}, { context, timeMs, sizeBytes, bitrate, speed, frame, fps, quality in
|
|
53
|
+
guard let context else { return }
|
|
54
|
+
let session = Unmanaged<Session>.fromOpaque(context).takeUnretainedValue()
|
|
55
|
+
session.onStatistics?(timeMs, sizeBytes, bitrate, speed, frame, fps, quality)
|
|
56
|
+
}, nil)
|
|
38
57
|
|
|
39
58
|
final class HybridMunimFfmpeg: HybridMunimFfmpegSpec {
|
|
40
59
|
private var sessionCounter: Double = 0
|
|
@@ -88,26 +107,28 @@ final class HybridMunimFfmpeg: HybridMunimFfmpegSpec {
|
|
|
88
107
|
// its logger, so it is captured to a file and appended to the output.
|
|
89
108
|
let printedPath = Self.temporaryFile()
|
|
90
109
|
|
|
91
|
-
installCallbacks
|
|
92
|
-
|
|
93
|
-
active.onLog = onLog
|
|
94
|
-
active.onStatistics = onStatistics
|
|
110
|
+
_ = installCallbacks
|
|
111
|
+
let session = Session(onLog: onLog, onStatistics: onStatistics)
|
|
95
112
|
|
|
96
|
-
let returnCode =
|
|
97
|
-
|
|
113
|
+
let returnCode = withExtendedLifetime(session) {
|
|
114
|
+
withArrayOfCStrings(arguments_) { argv in
|
|
115
|
+
munim_ffmpeg_execute_ctx(
|
|
116
|
+
Int32(arguments_.count),
|
|
117
|
+
argv,
|
|
118
|
+
printedPath,
|
|
119
|
+
Unmanaged.passUnretained(session).toOpaque()
|
|
120
|
+
)
|
|
121
|
+
}
|
|
98
122
|
}
|
|
99
123
|
|
|
100
124
|
let printed = (try? String(contentsOfFile: printedPath, encoding: .utf8)) ?? ""
|
|
101
125
|
try? FileManager.default.removeItem(atPath: printedPath)
|
|
102
126
|
|
|
103
|
-
let output = active.output + printed
|
|
104
|
-
active.reset()
|
|
105
|
-
|
|
106
127
|
promise.resolve(
|
|
107
128
|
withResult: self.result(
|
|
108
129
|
sessionId: sessionId,
|
|
109
130
|
returnCode: returnCode,
|
|
110
|
-
output: output,
|
|
131
|
+
output: session.output + printed,
|
|
111
132
|
startedAt: startedAt
|
|
112
133
|
)
|
|
113
134
|
)
|
|
@@ -178,21 +199,24 @@ final class HybridMunimFfmpeg: HybridMunimFfmpegSpec {
|
|
|
178
199
|
) -> (Int32, String) {
|
|
179
200
|
let destination = temporaryFile()
|
|
180
201
|
|
|
181
|
-
installCallbacks
|
|
182
|
-
|
|
183
|
-
active.onLog = onLog
|
|
202
|
+
_ = installCallbacks
|
|
203
|
+
let session = Session(onLog: onLog)
|
|
184
204
|
|
|
185
|
-
let returnCode =
|
|
186
|
-
|
|
205
|
+
let returnCode = withExtendedLifetime(session) {
|
|
206
|
+
withArrayOfCStrings(arguments) { argv in
|
|
207
|
+
munim_ffmpeg_probe_ctx(
|
|
208
|
+
Int32(arguments.count),
|
|
209
|
+
argv,
|
|
210
|
+
destination,
|
|
211
|
+
Unmanaged.passUnretained(session).toOpaque()
|
|
212
|
+
)
|
|
213
|
+
}
|
|
187
214
|
}
|
|
188
215
|
|
|
189
216
|
let report = (try? String(contentsOfFile: destination, encoding: .utf8)) ?? ""
|
|
190
217
|
try? FileManager.default.removeItem(atPath: destination)
|
|
191
218
|
|
|
192
|
-
|
|
193
|
-
active.reset()
|
|
194
|
-
|
|
195
|
-
return (returnCode, report.isEmpty ? logs : report)
|
|
219
|
+
return (returnCode, report.isEmpty ? session.output : report)
|
|
196
220
|
}
|
|
197
221
|
|
|
198
222
|
func cancel(sessionId: Double?) throws {
|
package/ios/munim_ffmpeg_core.h
CHANGED
|
@@ -21,7 +21,12 @@ typedef void (*munim_statistics_callback)(void *context, double time_ms,
|
|
|
21
21
|
/** Version string of the linked FFmpeg, e.g. "9.0.1". */
|
|
22
22
|
const char *munim_ffmpeg_version(void);
|
|
23
23
|
|
|
24
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Installs the callback functions. `context` is the default value handed to
|
|
26
|
+
* them; an execution started through one of the `_ctx` variants overrides it
|
|
27
|
+
* for exactly as long as that execution holds the core's lock, which is how
|
|
28
|
+
* callers waiting concurrently keep their callbacks apart.
|
|
29
|
+
*/
|
|
25
30
|
void munim_ffmpeg_set_callbacks(munim_log_callback on_log,
|
|
26
31
|
munim_statistics_callback on_statistics,
|
|
27
32
|
void *context);
|
|
@@ -38,10 +43,18 @@ void munim_ffmpeg_set_callbacks(munim_log_callback on_log,
|
|
|
38
43
|
int munim_ffmpeg_execute(int argc, const char *const *argv,
|
|
39
44
|
const char *stdout_path);
|
|
40
45
|
|
|
46
|
+
/** Like munim_ffmpeg_execute, with a per-run callback context. */
|
|
47
|
+
int munim_ffmpeg_execute_ctx(int argc, const char *const *argv,
|
|
48
|
+
const char *stdout_path, void *session);
|
|
49
|
+
|
|
41
50
|
/** Runs `ffprobe`, writing its report to `output_path` via `-o`. */
|
|
42
51
|
int munim_ffmpeg_probe(int argc, const char *const *argv,
|
|
43
52
|
const char *output_path);
|
|
44
53
|
|
|
54
|
+
/** Like munim_ffmpeg_probe, with a per-run callback context. */
|
|
55
|
+
int munim_ffmpeg_probe_ctx(int argc, const char *const *argv,
|
|
56
|
+
const char *output_path, void *session);
|
|
57
|
+
|
|
45
58
|
/**
|
|
46
59
|
* Requests cancellation of the running execution, and of any execution already
|
|
47
60
|
* queued behind it.
|
package/lib/index.js
CHANGED
|
@@ -39,9 +39,9 @@ export function cancelAll() {
|
|
|
39
39
|
export function getFFmpegVersion() {
|
|
40
40
|
return MunimFfmpeg.ffmpegVersion;
|
|
41
41
|
}
|
|
42
|
-
// The bundled FFmpeg builds differ per platform: Android ships
|
|
43
|
-
// iOS ships
|
|
44
|
-
//
|
|
42
|
+
// The bundled FFmpeg builds differ per platform: Android ships the MediaCodec
|
|
43
|
+
// hardware encoders, iOS ships VideoToolbox. Asking the binary what it
|
|
44
|
+
// supports is more reliable than hard-coding a per-platform table.
|
|
45
45
|
const codecCache = new Map();
|
|
46
46
|
function listCodecs(flag) {
|
|
47
47
|
const cached = codecCache.get(flag);
|
package/package.json
CHANGED
package/scripts/binaries.json
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
/*
|
|
3
3
|
* Downloads the prebuilt FFmpeg binaries for this version of munim-ffmpeg.
|
|
4
4
|
*
|
|
5
|
-
* They are not published to npm: the bundle is
|
|
6
|
-
* shared objects, which does not belong in a registry tarball. It ships as a
|
|
5
|
+
* They are not published to npm: the bundle is well over 100 MB of static
|
|
6
|
+
* libraries and shared objects, which does not belong in a registry tarball. It ships as a
|
|
7
7
|
* GitHub release asset instead, pinned by the checksum in scripts/binaries.json
|
|
8
8
|
* so a build cannot silently pick up different bytes.
|
|
9
9
|
*
|
|
@@ -27,7 +27,7 @@ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
|
27
27
|
const REPOSITORY = 'munimtechnologies/munim-ffmpeg'
|
|
28
28
|
const MARKERS = [
|
|
29
29
|
'ios/MunimFFmpeg.xcframework',
|
|
30
|
-
'android/src/main/jniLibs/arm64-v8a/
|
|
30
|
+
'android/src/main/jniLibs/arm64-v8a/libmunimffmpeg.so',
|
|
31
31
|
]
|
|
32
32
|
|
|
33
33
|
async function exists(target) {
|
|
@@ -90,6 +90,14 @@ async function main() {
|
|
|
90
90
|
)
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
// A previous version may have left different files behind (older releases
|
|
94
|
+
// shipped several .so files per ABI), so the targets start empty.
|
|
95
|
+
for (const target of [
|
|
96
|
+
'ios/MunimFFmpeg.xcframework',
|
|
97
|
+
'android/src/main/jniLibs',
|
|
98
|
+
]) {
|
|
99
|
+
await rm(path.join(root, target), { force: true, recursive: true })
|
|
100
|
+
}
|
|
93
101
|
await run('tar', ['xzf', archive, '-C', root])
|
|
94
102
|
await rm(cache, { force: true, recursive: true })
|
|
95
103
|
|
package/src/index.ts
CHANGED
|
@@ -84,9 +84,9 @@ export function getFFmpegVersion(): string {
|
|
|
84
84
|
return MunimFfmpeg.ffmpegVersion
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
// The bundled FFmpeg builds differ per platform: Android ships
|
|
88
|
-
// iOS ships
|
|
89
|
-
//
|
|
87
|
+
// The bundled FFmpeg builds differ per platform: Android ships the MediaCodec
|
|
88
|
+
// hardware encoders, iOS ships VideoToolbox. Asking the binary what it
|
|
89
|
+
// supports is more reliable than hard-coding a per-platform table.
|
|
90
90
|
const codecCache = new Map<string, Promise<string[]>>()
|
|
91
91
|
|
|
92
92
|
function listCodecs(flag: '-encoders' | '-decoders'): Promise<string[]> {
|