munim-ffmpeg 0.5.1 โ†’ 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 | MediaCodec | Hardware on both, `libopenh264` as the software fallback; use `pickEncoder(['h264_videotoolbox', 'h264_mediacodec', 'libopenh264'])` instead of hard-coding an encoder. |
136
- | Remote HTTP(S) inputs | โœ… | โœ… | iOS links SecureTransport, Android links mbedTLS. 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,7 +585,11 @@ if (!result.success) {
567
585
  ```typescript
568
586
  import { execute, pickEncoder } from 'munim-ffmpeg'
569
587
 
570
- const encoder = await pickEncoder(['h264_videotoolbox', 'h264_mediacodec', 'libopenh264'])
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
595
  // MediaCodec wants NV12 input; the others take planar YUV.
@@ -608,6 +630,39 @@ const result = await execute([
608
630
  ])
609
631
  ```
610
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
+
611
666
  ### Burn subtitles into a video
612
667
 
613
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.
@@ -619,18 +674,24 @@ import { execute, normalizePath } from 'munim-ffmpeg'
619
674
  // shadows, positioning, karaoke โ€” everything the format supports.
620
675
  await execute([
621
676
  '-y',
622
- '-i', inputPath,
623
- '-vf', `ass=filename=${normalizePath(subtitlePath)}`,
624
- '-c:a', 'copy',
677
+ '-i',
678
+ inputPath,
679
+ '-vf',
680
+ `ass=filename=${normalizePath(subtitlePath)}`,
681
+ '-c:a',
682
+ 'copy',
625
683
  outputPath,
626
684
  ])
627
685
 
628
686
  // SRT can be styled at burn time with force_style.
629
687
  await execute([
630
688
  '-y',
631
- '-i', inputPath,
632
- '-vf', `subtitles=filename=${normalizePath(srtPath)}:force_style='Fontsize=28,PrimaryColour=&H00FFFF00,Outline=2'`,
633
- '-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',
634
695
  outputPath,
635
696
  ])
636
697
  ```
@@ -647,19 +708,114 @@ import { execute } from 'munim-ffmpeg'
647
708
  // Bundle one video, two audio languages, and a subtitle track into MKV.
648
709
  await execute([
649
710
  '-y',
650
- '-i', videoPath, '-i', urduAudioPath, '-i', subtitlePath,
651
- '-map', '0:v:0', '-map', '0:a:0', '-map', '1:a:0', '-map', '2:s:0',
652
- '-c:v', 'copy', '-c:a', 'aac', '-c:s', 'srt',
653
- '-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',
654
733
  outputMkvPath,
655
734
  ])
656
735
 
657
736
  // Extract the second audio track without re-encoding.
658
737
  await execute([
659
- '-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,
660
814
  ])
661
815
  ```
662
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
+
663
819
  ### Cancel a long-running command
664
820
 
665
821
  ```typescript
@@ -712,9 +868,9 @@ if (result.success) {
712
868
 
713
869
  The JavaScript, TypeScript, Swift, Kotlin, C core, and generated Nitro bridge in this repository are Apache-2.0.
714
870
 
715
- 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.
716
872
 
717
- > **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.
718
874
 
719
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.
720
876
 
@@ -773,7 +929,10 @@ Nitrogen output under `nitrogen/generated` is committed. Change the `.nitro.ts`
773
929
 
774
930
  ### Example app
775
931
 
776
- `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`.
777
936
 
778
937
  ```bash
779
938
  npm run example:ios
@@ -781,7 +940,7 @@ npm run example:ios
781
940
  npm run example:android
782
941
  ```
783
942
 
784
- 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.
785
944
 
786
945
  ### Rebuilding FFmpeg
787
946
 
@@ -794,14 +953,16 @@ See [`scripts/ffmpeg/README.md`](./scripts/ffmpeg/README.md) for what the build
794
953
 
795
954
  ### Releasing
796
955
 
797
- Releases run locally from a clean `main`; this repository does not use GitHub Actions.
956
+ Releases run locally from a clean `main`:
798
957
 
799
958
  ```bash
800
959
  npm run check
801
960
  npm run release:local
802
961
  ```
803
962
 
804
- `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).
805
966
 
806
967
  ## ๐Ÿ‘ Contributing
807
968
 
@@ -67,7 +67,7 @@ class FFmpegSession(
67
67
  object FFmpegNative {
68
68
  init {
69
69
  configureFontconfig()
70
- System.loadLibrary("munimffmpeg9")
70
+ System.loadLibrary("munimffmpeg")
71
71
  }
72
72
 
73
73
  /** Return code the tools report when a run was cancelled. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "munim-ffmpeg",
3
- "version": "0.5.1",
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": "e206478796cacf79936c4e1f8a5d969e5063f890cff10badfd13f39a67626f37",
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