munim-ffmpeg 0.6.0 → 0.7.1

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
@@ -94,7 +94,7 @@
94
94
  ### FFmpeg execution
95
95
 
96
96
  - 🎬 **Argument-array commands:** FFmpeg's own CLI code paths, without shell parsing or quoting
97
- - 🆕 **FFmpeg 9.0.1:** The current upstream release, identical on both platforms
97
+ - 🆕 **FFmpeg 9.0.2:** The current upstream release, identical on both platforms
98
98
  - ⚡ **Asynchronous sessions:** Keep the React Native thread responsive during native work
99
99
  - 📝 **Live logs:** Receive FFmpeg output as it is produced
100
100
  - 📈 **Encoding statistics:** Track time, size, bitrate, speed, frames, FPS, and quality
@@ -126,7 +126,7 @@
126
126
  | ---------------------------- | ------------ | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
127
127
  | FFmpeg argument execution | ✅ | ✅ | Commands run asynchronously through the native compatibility library. |
128
128
  | FFprobe argument execution | ✅ | ✅ | Custom FFprobe arguments return `FFmpegSessionResult`. |
129
- | Parsed media information | ✅ | ✅ | `getMediaInformation()` returns parsed FFprobe JSON. |
129
+ | Parsed media information | ✅ | ✅ | `getMediaInformation()` returns typed, parsed FFprobe JSON. |
130
130
  | Log callback | ✅ | ✅ | Logs are delivered while a session is active. |
131
131
  | Encoding-statistics callback | ✅ | ✅ | Available for FFmpeg execution. |
132
132
  | Immediate session ID | ✅ | ✅ | `onSessionCreated` fires after the native session is created. |
@@ -183,11 +183,11 @@ await execute([
183
183
 
184
184
  ## Bundled FFmpeg builds
185
185
 
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.
186
+ Both platforms run **FFmpeg 9.0.2**, 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.
187
187
 
188
188
  | | iOS | Android |
189
189
  | --------------- | ------------------------------------------------------- | ------------------------------- |
190
- | FFmpeg | 9.0.1 | 9.0.1 |
190
+ | FFmpeg | 9.0.2 | 9.0.2 |
191
191
  | Architectures | arm64 device, arm64 + x86_64 simulator | arm64-v8a, armeabi-v7a, x86_64 |
192
192
  | Hardware codecs | VideoToolbox, AudioToolbox | MediaCodec |
193
193
  | TLS | SecureTransport | mbedTLS |
@@ -436,10 +436,24 @@ function probe(
436
436
  Runs FFprobe for the format, streams, and chapters at a local media path, then parses its JSON response.
437
437
 
438
438
  ```typescript
439
- function getMediaInformation(path: string): Promise<unknown>
439
+ function getMediaInformation(path: string): Promise<MediaInformation>
440
440
  ```
441
441
 
442
- Applications should validate or narrow the returned JSON shape before using fields from it.
442
+ `MediaInformation` types the `format`, `streams`, and `chapters` sections of FFprobe's report. Every field is optional because FFprobe only prints what the container exposes, and numeric values such as `duration` and `bit_rate` arrive as strings, exactly as FFprobe prints them. Unknown keys are preserved under an index signature.
443
+
444
+ ```typescript
445
+ const info = await getMediaInformation(inputPath)
446
+ const video = info.streams?.find((stream) => stream.codec_type === 'video')
447
+ console.log(video?.width, video?.height, info.format?.duration)
448
+ ```
449
+
450
+ ### `getMediaDuration(information)`
451
+
452
+ Reads the duration in seconds from a `MediaInformation` report, falling back to the first stream that reports one. Returns `undefined` when FFprobe did not report a duration (live inputs, some raw streams).
453
+
454
+ ```typescript
455
+ function getMediaDuration(information: MediaInformation): number | undefined
456
+ ```
443
457
 
444
458
  ### `cancel(sessionId?)`
445
459
 
@@ -868,7 +882,7 @@ if (result.success) {
868
882
 
869
883
  The JavaScript, TypeScript, Swift, Kotlin, C core, and generated Nitro bridge in this repository are Apache-2.0.
870
884
 
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.
885
+ The bundled FFmpeg 9.0.2 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.
872
886
 
873
887
  > **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.
874
888
 
@@ -4,6 +4,9 @@ import androidx.annotation.Keep
4
4
  import com.facebook.proguard.annotations.DoNotStrip
5
5
  import com.margelo.nitro.core.Promise
6
6
  import java.io.File
7
+ import java.util.concurrent.Executors
8
+ import java.util.concurrent.ThreadFactory
9
+ import java.util.concurrent.atomic.AtomicInteger
7
10
  import java.util.concurrent.atomic.AtomicLong
8
11
 
9
12
  @Keep
@@ -16,6 +19,25 @@ class HybridMunimFfmpeg : HybridMunimFfmpegSpec() {
16
19
 
17
20
  private fun nextSession() = sessions.incrementAndGet().toDouble()
18
21
 
22
+ /**
23
+ * Runs [block] on a dedicated FFmpeg thread and settles a [Promise] with its
24
+ * outcome. Nitro's `Promise.async` shares Kotlin's default coroutine pool,
25
+ * which only has one thread per CPU core: a multi-minute transcode plus a
26
+ * few queued sessions would park the whole pool and starve every other
27
+ * coroutine in the app. FFmpeg work therefore gets its own unbounded pool.
28
+ */
29
+ private fun <T> runOnFfmpegThread(block: () -> T): Promise<T> {
30
+ val promise = Promise<T>()
31
+ executor.execute {
32
+ try {
33
+ promise.resolve(block())
34
+ } catch (error: Throwable) {
35
+ promise.reject(error)
36
+ }
37
+ }
38
+ return promise
39
+ }
40
+
19
41
  private fun result(
20
42
  sessionId: Double,
21
43
  returnCode: Int,
@@ -52,7 +74,7 @@ class HybridMunimFfmpeg : HybridMunimFfmpegSpec() {
52
74
  val sessionId = nextSession()
53
75
  onSessionCreated?.invoke(sessionId)
54
76
 
55
- return Promise.async {
77
+ return runOnFfmpegThread {
56
78
  val startedAt = System.currentTimeMillis()
57
79
  // ffmpeg prints reports such as -encoders and -protocols to stdout rather
58
80
  // than through its logger, so it is captured to a file and appended.
@@ -79,7 +101,7 @@ class HybridMunimFfmpeg : HybridMunimFfmpegSpec() {
79
101
  val sessionId = nextSession()
80
102
  onSessionCreated?.invoke(sessionId)
81
103
 
82
- return Promise.async {
104
+ return runOnFfmpegThread {
83
105
  val startedAt = System.currentTimeMillis()
84
106
  val (returnCode, report) = runProbe(arguments_, onLog)
85
107
  result(sessionId, returnCode, report, System.currentTimeMillis() - startedAt)
@@ -87,7 +109,7 @@ class HybridMunimFfmpeg : HybridMunimFfmpegSpec() {
87
109
  }
88
110
 
89
111
  override fun getMediaInformation(path: String): Promise<String> {
90
- return Promise.async {
112
+ return runOnFfmpegThread {
91
113
  val (returnCode, report) = runProbe(
92
114
  arrayOf(
93
115
  "-v",
@@ -155,4 +177,17 @@ class HybridMunimFfmpeg : HybridMunimFfmpegSpec() {
155
177
  override fun cancelAll() {
156
178
  FFmpegNative.nativeCancel()
157
179
  }
180
+
181
+ private companion object {
182
+ private val threadCounter = AtomicInteger(0)
183
+
184
+ /** Unbounded so a queued session never blocks anything but itself. */
185
+ private val executor = Executors.newCachedThreadPool(
186
+ ThreadFactory { runnable ->
187
+ Thread(runnable, "munim-ffmpeg-${threadCounter.incrementAndGet()}").apply {
188
+ isDaemon = true
189
+ }
190
+ },
191
+ )
192
+ }
158
193
  }
package/lib/index.d.ts CHANGED
@@ -13,7 +13,89 @@ export type { FFmpegLogCallback, FFmpegSessionResult, FFmpegSessionCreatedCallba
13
13
  export declare function normalizePath(value: string): string;
14
14
  export declare function execute(arguments_: string[], onLog?: FFmpegLogCallback, onStatistics?: FFmpegStatisticsCallback, onSessionCreated?: FFmpegSessionCreatedCallback): Promise<FFmpegSessionResult>;
15
15
  export declare function probe(arguments_: string[], onLog?: FFmpegLogCallback, onSessionCreated?: FFmpegSessionCreatedCallback): Promise<FFmpegSessionResult>;
16
- export declare function getMediaInformation(path: string): Promise<unknown>;
16
+ /**
17
+ * Shape of the FFprobe report returned by {@link getMediaInformation}.
18
+ *
19
+ * FFprobe's JSON output varies by container and codec, so every field is
20
+ * optional and unknown keys are preserved. Numeric values such as durations
21
+ * and bit rates arrive as strings, exactly as FFprobe prints them.
22
+ */
23
+ export interface MediaStream {
24
+ index: number;
25
+ codec_type?: 'video' | 'audio' | 'subtitle' | 'data' | 'attachment' | string;
26
+ codec_name?: string;
27
+ codec_long_name?: string;
28
+ profile?: string;
29
+ codec_tag_string?: string;
30
+ codec_tag?: string;
31
+ width?: number;
32
+ height?: number;
33
+ coded_width?: number;
34
+ coded_height?: number;
35
+ pix_fmt?: string;
36
+ color_range?: string;
37
+ color_space?: string;
38
+ color_transfer?: string;
39
+ color_primaries?: string;
40
+ field_order?: string;
41
+ level?: number;
42
+ has_b_frames?: number;
43
+ sample_aspect_ratio?: string;
44
+ display_aspect_ratio?: string;
45
+ r_frame_rate?: string;
46
+ avg_frame_rate?: string;
47
+ time_base?: string;
48
+ start_pts?: number;
49
+ start_time?: string;
50
+ duration_ts?: number;
51
+ duration?: string;
52
+ bit_rate?: string;
53
+ max_bit_rate?: string;
54
+ bits_per_raw_sample?: string;
55
+ nb_frames?: string;
56
+ sample_fmt?: string;
57
+ sample_rate?: string;
58
+ channels?: number;
59
+ channel_layout?: string;
60
+ bits_per_sample?: number;
61
+ disposition?: Record<string, number>;
62
+ tags?: Record<string, string>;
63
+ side_data_list?: Array<Record<string, unknown>>;
64
+ [key: string]: unknown;
65
+ }
66
+ export interface MediaFormat {
67
+ filename?: string;
68
+ nb_streams?: number;
69
+ nb_programs?: number;
70
+ format_name?: string;
71
+ format_long_name?: string;
72
+ start_time?: string;
73
+ duration?: string;
74
+ size?: string;
75
+ bit_rate?: string;
76
+ probe_score?: number;
77
+ tags?: Record<string, string>;
78
+ [key: string]: unknown;
79
+ }
80
+ export interface MediaChapter {
81
+ id: number;
82
+ time_base?: string;
83
+ start?: number;
84
+ start_time?: string;
85
+ end?: number;
86
+ end_time?: string;
87
+ tags?: Record<string, string>;
88
+ [key: string]: unknown;
89
+ }
90
+ export interface MediaInformation {
91
+ format?: MediaFormat;
92
+ streams?: MediaStream[];
93
+ chapters?: MediaChapter[];
94
+ [key: string]: unknown;
95
+ }
96
+ export declare function getMediaInformation(path: string): Promise<MediaInformation>;
97
+ /** Duration in seconds from a {@link MediaInformation} report, if FFprobe reported one. */
98
+ export declare function getMediaDuration(information: MediaInformation): number | undefined;
17
99
  export declare function cancel(sessionId?: number): void;
18
100
  export declare function cancelAll(): void;
19
101
  export declare function getFFmpegVersion(): string;
package/lib/index.js CHANGED
@@ -30,6 +30,16 @@ export function probe(arguments_, onLog, onSessionCreated) {
30
30
  export function getMediaInformation(path) {
31
31
  return MunimFfmpeg.getMediaInformation(normalizePath(path)).then((value) => JSON.parse(value));
32
32
  }
33
+ /** Duration in seconds from a {@link MediaInformation} report, if FFprobe reported one. */
34
+ export function getMediaDuration(information) {
35
+ const raw = information.format?.duration ??
36
+ information.streams?.find((stream) => stream.duration !== undefined)
37
+ ?.duration;
38
+ if (raw === undefined)
39
+ return undefined;
40
+ const seconds = Number(raw);
41
+ return Number.isFinite(seconds) ? seconds : undefined;
42
+ }
33
43
  export function cancel(sessionId) {
34
44
  MunimFfmpeg.cancel(sessionId);
35
45
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "munim-ffmpeg",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
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",
@@ -45,7 +45,7 @@
45
45
  "specs": "npm run codegen",
46
46
  "build": "npm run typecheck && tsc",
47
47
  "prepack": "npm run build",
48
- "check": "npm run codegen && npm run typecheck && npm run typecheck:example && npm run build && npm pack --dry-run",
48
+ "check": "npm run codegen && npm run typecheck && npm run build && npm run typecheck:example && npm pack --dry-run",
49
49
  "example:start": "npm --workspace example run start",
50
50
  "example:ios": "npm --workspace example run ios",
51
51
  "example:android": "npm --workspace example run android",
@@ -118,8 +118,8 @@
118
118
  "nitrogen": "0.36.5",
119
119
  "prettier": "^3.8.3",
120
120
  "react": "19.2.3",
121
- "react-native": "0.86.2",
122
- "react-native-nitro-modules": "^0.36.5",
121
+ "react-native": "0.86.3",
122
+ "react-native-nitro-modules": "0.36.5",
123
123
  "semantic-release": "^25.0.9",
124
124
  "typescript": "^6.0.3",
125
125
  "typescript-eslint": "^8.46.0"
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "archive": "munim-ffmpeg-binaries.tar.gz",
3
- "sha256": "992527af6fd69672cd863501371a835cad6983c45be5525b45a8462848fe7ecc",
4
- "ffmpeg": "9.0.1"
3
+ "sha256": "6d709ca6ddf428743c29d54325cca34c3335d771076690472bb036c2d849e64f",
4
+ "ffmpeg": "9.0.2"
5
5
  }
package/src/index.ts CHANGED
@@ -66,12 +66,109 @@ export function probe(
66
66
  )
67
67
  }
68
68
 
69
- export function getMediaInformation(path: string): Promise<unknown> {
70
- return MunimFfmpeg.getMediaInformation(normalizePath(path)).then((value) =>
71
- JSON.parse(value)
69
+ /**
70
+ * Shape of the FFprobe report returned by {@link getMediaInformation}.
71
+ *
72
+ * FFprobe's JSON output varies by container and codec, so every field is
73
+ * optional and unknown keys are preserved. Numeric values such as durations
74
+ * and bit rates arrive as strings, exactly as FFprobe prints them.
75
+ */
76
+ export interface MediaStream {
77
+ index: number
78
+ codec_type?: 'video' | 'audio' | 'subtitle' | 'data' | 'attachment' | string
79
+ codec_name?: string
80
+ codec_long_name?: string
81
+ profile?: string
82
+ codec_tag_string?: string
83
+ codec_tag?: string
84
+ width?: number
85
+ height?: number
86
+ coded_width?: number
87
+ coded_height?: number
88
+ pix_fmt?: string
89
+ color_range?: string
90
+ color_space?: string
91
+ color_transfer?: string
92
+ color_primaries?: string
93
+ field_order?: string
94
+ level?: number
95
+ has_b_frames?: number
96
+ sample_aspect_ratio?: string
97
+ display_aspect_ratio?: string
98
+ r_frame_rate?: string
99
+ avg_frame_rate?: string
100
+ time_base?: string
101
+ start_pts?: number
102
+ start_time?: string
103
+ duration_ts?: number
104
+ duration?: string
105
+ bit_rate?: string
106
+ max_bit_rate?: string
107
+ bits_per_raw_sample?: string
108
+ nb_frames?: string
109
+ sample_fmt?: string
110
+ sample_rate?: string
111
+ channels?: number
112
+ channel_layout?: string
113
+ bits_per_sample?: number
114
+ disposition?: Record<string, number>
115
+ tags?: Record<string, string>
116
+ side_data_list?: Array<Record<string, unknown>>
117
+ [key: string]: unknown
118
+ }
119
+
120
+ export interface MediaFormat {
121
+ filename?: string
122
+ nb_streams?: number
123
+ nb_programs?: number
124
+ format_name?: string
125
+ format_long_name?: string
126
+ start_time?: string
127
+ duration?: string
128
+ size?: string
129
+ bit_rate?: string
130
+ probe_score?: number
131
+ tags?: Record<string, string>
132
+ [key: string]: unknown
133
+ }
134
+
135
+ export interface MediaChapter {
136
+ id: number
137
+ time_base?: string
138
+ start?: number
139
+ start_time?: string
140
+ end?: number
141
+ end_time?: string
142
+ tags?: Record<string, string>
143
+ [key: string]: unknown
144
+ }
145
+
146
+ export interface MediaInformation {
147
+ format?: MediaFormat
148
+ streams?: MediaStream[]
149
+ chapters?: MediaChapter[]
150
+ [key: string]: unknown
151
+ }
152
+
153
+ export function getMediaInformation(path: string): Promise<MediaInformation> {
154
+ return MunimFfmpeg.getMediaInformation(normalizePath(path)).then(
155
+ (value) => JSON.parse(value) as MediaInformation
72
156
  )
73
157
  }
74
158
 
159
+ /** Duration in seconds from a {@link MediaInformation} report, if FFprobe reported one. */
160
+ export function getMediaDuration(
161
+ information: MediaInformation
162
+ ): number | undefined {
163
+ const raw =
164
+ information.format?.duration ??
165
+ information.streams?.find((stream) => stream.duration !== undefined)
166
+ ?.duration
167
+ if (raw === undefined) return undefined
168
+ const seconds = Number(raw)
169
+ return Number.isFinite(seconds) ? seconds : undefined
170
+ }
171
+
75
172
  export function cancel(sessionId?: number): void {
76
173
  MunimFfmpeg.cancel(sessionId)
77
174
  }