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 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 | Android | Notes |
123
- | ---------------------------- | --------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
124
- | FFmpeg argument execution | โœ… | โœ… | Commands run asynchronously through the native compatibility library. |
125
- | FFprobe argument execution | โœ… | โœ… | Custom FFprobe arguments return `FFmpegSessionResult`. |
126
- | Parsed media information | โœ… | โœ… | `getMediaInformation()` returns parsed FFprobe JSON. |
127
- | Log callback | โœ… | โœ… | Logs are delivered while a session is active. |
128
- | Encoding-statistics callback | โœ… | โœ… | Available for FFmpeg execution. |
129
- | Immediate session ID | โœ… | โœ… | `onSessionCreated` fires after the native session is created. |
130
- | Cancel one FFmpeg session | โœ… | โœ… | Pass the positive safe-integer ID received by `execute`'s `onSessionCreated`. The native dependency does not expose FFprobe cancellation. |
131
- | Cancel all FFmpeg sessions | โœ… | โœ… | Use `cancelAll()` or call `cancel()` without an ID. |
132
- | Expo Go | โŒ | โŒ | A native development build is required. |
133
- | Capability discovery | โœ… | โœ… | `listEncoders()`, `listDecoders()`, `listMuxers()`, `listDemuxers()`, `listFilters()`, `listProtocols()`, and `pickEncoder()` report what the bundled build supports. |
134
- | Subtitle burn-in | โœ… | โœ… | libass with system fonts: Core Text on iOS, fontconfig over `/system/fonts` on Android. |
135
- | H.264 encoding | VideoToolbox | libx264 | The builds differ; use `pickEncoder(['libx264', 'h264_videotoolbox'])` instead of hard-coding an encoder. |
136
- | Remote HTTP(S) inputs | โœ… | โœ… | Both builds link GnuTLS. Remote server behaviour still varies; prefer local files for predictable app workflows. |
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 | Result |
145
- | --- | --- |
146
- | iPad Air (M3), iOS 26 | 25/25 |
147
- | iOS Simulator, arm64 | 25/25 |
148
- | Galaxy A14 5G, arm64-v8a | 25/25 |
149
- | Android emulator, arm64 | Software encoding passes; hardware encoding does not โ€” see below |
150
- | Android emulator, `x86_64` | Same: `libopenh264` passes, MediaCodec does not |
151
- | Android `armeabi-v7a` | Built and statically checked, not executed |
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 *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:
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(['-y', '-i', input, '-c:v', 'libopenh264', '-pix_fmt', 'yuv420p', output])
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
- | | iOS | Android |
175
- | --- | --- | --- |
176
- | FFmpeg | 9.0.1 | 9.0.1 |
177
- | Architectures | arm64 device, arm64 + x86_64 simulator | arm64-v8a, armeabi-v7a, x86_64 |
178
- | Hardware codecs | VideoToolbox, AudioToolbox | MediaCodec |
179
- | TLS | SecureTransport | mbedTLS |
180
- | Minimum | iOS 15.1 | API 24, 16 KB pages |
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 | iOS | Android |
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 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`.
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(['libx264', 'h264_videotoolbox'])
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
- // -preset is an x264 option; VideoToolbox rejects it.
574
- const quality = encoder === 'libx264' ? ['-preset', 'veryfast', '-crf', '23'] : ['-b:v', '2M']
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', inputPath,
622
- '-vf', `ass=filename=${normalizePath(subtitlePath)}`,
623
- '-c:a', 'copy',
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', inputPath,
631
- '-vf', `subtitles=filename=${normalizePath(srtPath)}:force_style='Fontsize=28,PrimaryColour=&H00FFFF00,Outline=2'`,
632
- '-c:a', 'copy',
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', videoPath, '-i', urduAudioPath, '-i', subtitlePath,
650
- '-map', '0:v:0', '-map', '0:a:0', '-map', '1:a:0', '-map', '2:s:0',
651
- '-c:v', 'copy', '-c:a', 'aac', '-c:s', 'srt',
652
- '-metadata:s:a:1', 'language=urd',
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', '-i', outputMkvPath, '-map', '0:a:1', '-c', 'copy', trackPath,
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 *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.
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 `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).
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 that runs a 25-check device suite: H.264 and HEVC encoding, VP9/Opus in WebM, MP3, AAC, scaling and multi-step filter graphs, software H.264 via openh264, 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`.
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 the suite on a physical device.
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`; this repository does not use GitHub Actions.
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 it from โ€” so run `npm run binaries:package` first.
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("munimffmpeg9")
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 returnCode = FFmpegNative.withCallbacks(
63
- onLog = { message ->
64
- output.append(message)
65
- onLog?.invoke(message)
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.toString(), System.currentTimeMillis() - startedAt)
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 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
- }
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 { logs.toString() }
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
- /// 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 = ""
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 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
- }
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
- active.reset()
93
- active.onLog = onLog
94
- active.onStatistics = onStatistics
110
+ _ = installCallbacks
111
+ let session = Session(onLog: onLog, onStatistics: onStatistics)
95
112
 
96
- let returnCode = withArrayOfCStrings(arguments_) { argv in
97
- munim_ffmpeg_execute(Int32(arguments_.count), argv, printedPath)
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
- active.reset()
183
- active.onLog = onLog
202
+ _ = installCallbacks
203
+ let session = Session(onLog: onLog)
184
204
 
185
- let returnCode = withArrayOfCStrings(arguments) { argv in
186
- munim_ffmpeg_probe(Int32(arguments.count), argv, destination)
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
- let logs = active.output
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 {
@@ -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
- /** Callbacks apply to whichever execution is currently running. */
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 libx264/libx265,
43
- // iOS ships the VideoToolbox hardware encoders instead. Asking the binary what
44
- // it supports is more reliable than hard-coding a per-platform table.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "munim-ffmpeg",
3
- "version": "0.5.0",
3
+ "version": "0.6.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",
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "archive": "munim-ffmpeg-binaries.tar.gz",
3
- "sha256": "05bb0a21348ae3958ca8b06fe6e31bfe2048277f6c6a4720271ce53a8973b653",
3
+ "sha256": "992527af6fd69672cd863501371a835cad6983c45be5525b45a8462848fe7ecc",
4
4
  "ffmpeg": "9.0.1"
5
5
  }
@@ -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 ~200 MB of static libraries and
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/libmunimffmpeg9.so',
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 libx264/libx265,
88
- // iOS ships the VideoToolbox hardware encoders instead. Asking the binary what
89
- // it supports is more reliable than hard-coding a per-platform table.
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[]> {