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 +21 -7
- package/android/src/main/java/com/margelo/nitro/munimffmpeg/HybridMunimFfmpeg.kt +38 -3
- package/lib/index.d.ts +83 -1
- package/lib/index.js +10 -0
- package/package.json +4 -4
- package/scripts/binaries.json +2 -2
- package/src/index.ts +100 -3
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.
|
|
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.
|
|
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.
|
|
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<
|
|
439
|
+
function getMediaInformation(path: string): Promise<MediaInformation>
|
|
440
440
|
```
|
|
441
441
|
|
|
442
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
122
|
-
"react-native-nitro-modules": "
|
|
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"
|
package/scripts/binaries.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -66,12 +66,109 @@ export function probe(
|
|
|
66
66
|
)
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
}
|