munim-ffmpeg 0.1.1 → 0.3.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.
@@ -1,19 +1,39 @@
1
1
  package com.margelo.nitro.munimffmpeg
2
2
 
3
3
  import androidx.annotation.Keep
4
- import com.arthenica.ffmpegkit.FFmpegKit
5
- import com.arthenica.ffmpegkit.FFmpegKitConfig
6
- import com.arthenica.ffmpegkit.FFprobeKit
7
- import com.arthenica.ffmpegkit.ReturnCode
8
- import com.arthenica.ffmpegkit.Session
9
4
  import com.facebook.proguard.annotations.DoNotStrip
10
5
  import com.margelo.nitro.core.Promise
6
+ import java.io.File
7
+ import java.util.concurrent.atomic.AtomicLong
11
8
 
12
9
  @Keep
13
10
  @DoNotStrip
14
11
  class HybridMunimFfmpeg : HybridMunimFfmpegSpec() {
15
12
  override val ffmpegVersion: String
16
- get() = FFmpegKitConfig.getFFmpegVersion()
13
+ get() = FFmpegNative.nativeVersion()
14
+
15
+ private val sessions = AtomicLong(0)
16
+
17
+ private fun nextSession() = sessions.incrementAndGet().toDouble()
18
+
19
+ private fun result(
20
+ sessionId: Double,
21
+ returnCode: Int,
22
+ output: String,
23
+ durationMs: Long,
24
+ ): FFmpegSessionResult {
25
+ val cancelled = returnCode == FFmpegNative.CANCELLED
26
+ return FFmpegSessionResult(
27
+ sessionId = sessionId,
28
+ returnCode = returnCode.toDouble(),
29
+ success = returnCode == 0,
30
+ cancelled = cancelled,
31
+ state = "completed",
32
+ durationMs = durationMs.toDouble(),
33
+ output = output,
34
+ failStackTrace = null,
35
+ )
36
+ }
17
37
 
18
38
  override fun execute(
19
39
  arguments_: Array<String>,
@@ -29,31 +49,42 @@ class HybridMunimFfmpeg : HybridMunimFfmpegSpec() {
29
49
  ) -> Unit)?,
30
50
  onSessionCreated: ((sessionId: Double) -> Unit)?,
31
51
  ): Promise<FFmpegSessionResult> {
32
- val promise = Promise<FFmpegSessionResult>()
52
+ val sessionId = nextSession()
53
+ onSessionCreated?.invoke(sessionId)
33
54
 
34
- try {
35
- val session = FFmpegKit.executeWithArgumentsAsync(
36
- arguments_,
37
- { session -> promise.resolve(session.toResult()) },
38
- { log -> onLog?.invoke(log.message) },
39
- { statistics ->
55
+ return Promise.async {
56
+ val output = StringBuilder()
57
+ val startedAt = System.currentTimeMillis()
58
+ // ffmpeg prints reports such as -encoders and -protocols to stdout rather
59
+ // than through its logger, so it is captured to a file and appended.
60
+ val stdout = File.createTempFile("munim-ffmpeg", ".txt")
61
+
62
+ val returnCode = FFmpegNative.withCallbacks(
63
+ onLog = { message ->
64
+ output.append(message)
65
+ onLog?.invoke(message)
66
+ },
67
+ onStatistics = { statistics ->
40
68
  onStatistics?.invoke(
41
- statistics.time,
42
- statistics.size.toDouble(),
43
- statistics.bitrate,
69
+ statistics.timeMs,
70
+ statistics.sizeBytes,
71
+ statistics.bitrateKbits,
44
72
  statistics.speed,
45
- statistics.videoFrameNumber.toDouble(),
46
- statistics.videoFps.toDouble(),
47
- statistics.videoQuality.toDouble(),
73
+ statistics.videoFrameNumber,
74
+ statistics.fps,
75
+ statistics.quality,
48
76
  )
49
77
  },
50
- )
51
- onSessionCreated?.invoke(session.sessionId.toDouble())
52
- } catch (error: Throwable) {
53
- promise.reject(error)
54
- }
78
+ ) {
79
+ FFmpegNative.nativeExecute(arguments_, stdout.absolutePath)
80
+ }
81
+
82
+ val printed = runCatching { stdout.readText() }.getOrDefault("")
83
+ stdout.delete()
84
+ output.append(printed)
55
85
 
56
- return promise
86
+ result(sessionId, returnCode, output.toString(), System.currentTimeMillis() - startedAt)
87
+ }
57
88
  }
58
89
 
59
90
  override fun probe(
@@ -61,25 +92,19 @@ class HybridMunimFfmpeg : HybridMunimFfmpegSpec() {
61
92
  onLog: ((message: String) -> Unit)?,
62
93
  onSessionCreated: ((sessionId: Double) -> Unit)?,
63
94
  ): Promise<FFmpegSessionResult> {
64
- val promise = Promise<FFmpegSessionResult>()
95
+ val sessionId = nextSession()
96
+ onSessionCreated?.invoke(sessionId)
65
97
 
66
- try {
67
- val session = FFprobeKit.executeWithArgumentsAsync(
68
- arguments_,
69
- { session -> promise.resolve(session.toResult()) },
70
- { log -> onLog?.invoke(log.message) },
71
- )
72
- onSessionCreated?.invoke(session.sessionId.toDouble())
73
- } catch (error: Throwable) {
74
- promise.reject(error)
98
+ return Promise.async {
99
+ val startedAt = System.currentTimeMillis()
100
+ val (returnCode, report) = runProbe(arguments_, onLog)
101
+ result(sessionId, returnCode, report, System.currentTimeMillis() - startedAt)
75
102
  }
76
-
77
- return promise
78
103
  }
79
104
 
80
105
  override fun getMediaInformation(path: String): Promise<String> {
81
106
  return Promise.async {
82
- val session = FFprobeKit.executeWithArguments(
107
+ val (returnCode, report) = runProbe(
83
108
  arrayOf(
84
109
  "-v",
85
110
  "error",
@@ -90,25 +115,49 @@ class HybridMunimFfmpeg : HybridMunimFfmpegSpec() {
90
115
  "-show_chapters",
91
116
  path,
92
117
  ),
118
+ null,
93
119
  )
94
- val returnCode = session.returnCode
95
120
 
96
- if (!ReturnCode.isSuccess(returnCode)) {
121
+ if (returnCode != 0) {
97
122
  throw IllegalStateException(
98
- session.failStackTrace ?: session.output.ifEmpty {
99
- "FFprobe failed with return code ${returnCode?.value ?: -1}"
100
- },
123
+ report.ifEmpty { "FFprobe failed with return code $returnCode" }
101
124
  )
102
125
  }
103
126
 
104
- session.output
127
+ report
128
+ }
129
+ }
130
+
131
+ /**
132
+ * ffprobe writes its report to stdout, which is not reachable from an app, so
133
+ * it is pointed at a temporary file with `-o` and read back.
134
+ */
135
+ private fun runProbe(
136
+ arguments: Array<String>,
137
+ onLog: ((message: String) -> Unit)?,
138
+ ): Pair<Int, String> {
139
+ val destination = File.createTempFile("munim-ffprobe", ".txt")
140
+ 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
+ }
151
+
152
+ val report = destination.readText()
153
+ return returnCode to report.ifEmpty { logs.toString() }
154
+ } finally {
155
+ destination.delete()
105
156
  }
106
157
  }
107
158
 
108
159
  override fun cancel(sessionId: Double?) {
109
- if (sessionId == null) {
110
- FFmpegKit.cancel()
111
- } else {
160
+ if (sessionId != null) {
112
161
  require(
113
162
  sessionId.isFinite() &&
114
163
  sessionId > 0 &&
@@ -117,25 +166,13 @@ class HybridMunimFfmpeg : HybridMunimFfmpegSpec() {
117
166
  ) {
118
167
  "Invalid FFmpeg session ID: $sessionId. Expected a positive safe integer."
119
168
  }
120
- FFmpegKit.cancel(sessionId.toLong())
121
169
  }
170
+ // Only one execution runs at a time, so a targeted cancel and cancelAll()
171
+ // are the same operation.
172
+ FFmpegNative.nativeCancel()
122
173
  }
123
174
 
124
175
  override fun cancelAll() {
125
- FFmpegKit.cancel()
126
- }
127
-
128
- private fun Session.toResult(): FFmpegSessionResult {
129
- val code = returnCode
130
- return FFmpegSessionResult(
131
- sessionId = sessionId.toDouble(),
132
- returnCode = (code?.value ?: -1).toDouble(),
133
- success = ReturnCode.isSuccess(code),
134
- cancelled = ReturnCode.isCancel(code),
135
- state = state.name.lowercase(),
136
- durationMs = duration.toDouble(),
137
- output = output,
138
- failStackTrace = failStackTrace,
139
- )
176
+ FFmpegNative.nativeCancel()
140
177
  }
141
178
  }
package/app.plugin.js CHANGED
@@ -1,8 +1,28 @@
1
- const { withGradleProperties } = require('expo/config-plugins')
1
+ const fs = require('node:fs')
2
+ const path = require('node:path')
3
+ const {
4
+ withDangerousMod,
5
+ withGradleProperties,
6
+ } = require('expo/config-plugins')
2
7
 
3
8
  const PICK_FIRSTS_PROPERTY = 'android.packagingOptions.pickFirsts'
4
9
  const CXX_SHARED_LIBRARY = '**/libc++_shared.so'
5
10
 
11
+ const PODFILE_MARKER = '# munim-ffmpeg: allow arm64 iOS Simulator builds'
12
+ const PODFILE_SNIPPET = ` ${PODFILE_MARKER}
13
+ # The FFmpegKit pod excludes arm64 from Simulator builds, which breaks Apple
14
+ # Silicon Macs even though its xcframework ships an arm64 Simulator slice.
15
+ installer.pods_project.build_configurations.each do |config|
16
+ config.build_settings.delete('EXCLUDED_ARCHS[sdk=iphonesimulator*]')
17
+ end
18
+ installer.aggregate_targets.each do |aggregate_target|
19
+ aggregate_target.xcconfigs.each do |config_name, xcconfig|
20
+ xcconfig.attributes.delete('EXCLUDED_ARCHS[sdk=iphonesimulator*]')
21
+ xcconfig.save_as(Pathname.new(aggregate_target.xcconfig_path(config_name)))
22
+ end
23
+ end
24
+ `
25
+
6
26
  function withAndroidPackaging(config) {
7
27
  return withGradleProperties(config, (gradleConfig) => {
8
28
  const existing = gradleConfig.modResults.find(
@@ -30,8 +50,44 @@ function withAndroidPackaging(config) {
30
50
  })
31
51
  }
32
52
 
53
+ function withSimulatorArchitectures(config) {
54
+ return withDangerousMod(config, [
55
+ 'ios',
56
+ (modConfig) => {
57
+ const podfilePath = path.join(
58
+ modConfig.modRequest.platformProjectRoot,
59
+ 'Podfile'
60
+ )
61
+
62
+ if (!fs.existsSync(podfilePath)) return modConfig
63
+
64
+ const contents = fs.readFileSync(podfilePath, 'utf8')
65
+ if (contents.includes(PODFILE_MARKER)) return modConfig
66
+
67
+ const postInstall = /^([ \t]*)post_install do \|(\w+)\|[ \t]*$/m
68
+ const match = contents.match(postInstall)
69
+
70
+ if (!match) {
71
+ console.warn(
72
+ 'munim-ffmpeg: no post_install block found in the Podfile; arm64 Simulator builds may fail.'
73
+ )
74
+ return modConfig
75
+ }
76
+
77
+ const snippet = PODFILE_SNIPPET.replace(/\binstaller\b/g, match[2])
78
+ const patched = contents.replace(
79
+ postInstall,
80
+ (line) => `${line}\n${snippet}`
81
+ )
82
+
83
+ fs.writeFileSync(podfilePath, patched)
84
+ return modConfig
85
+ },
86
+ ])
87
+ }
88
+
33
89
  module.exports = function withMunimFfmpeg(config) {
34
- return withAndroidPackaging(config)
90
+ return withSimulatorArchitectures(withAndroidPackaging(config))
35
91
  }
36
92
 
37
93
  module.exports.default = module.exports
package/ios/Bridge.h CHANGED
@@ -2,7 +2,10 @@
2
2
  // Bridge.h
3
3
  // NitroMunimFfmpeg
4
4
  //
5
- // Created by Marc Rousavy on 22.07.24.
5
+ // Objective-C bridging header: exposes the C core that drives FFmpeg 9's
6
+ // in-process command-line tools to the Swift implementation.
6
7
  //
7
8
 
8
9
  #pragma once
10
+
11
+ #include "munim_ffmpeg_core.h"
@@ -1,9 +1,75 @@
1
+ import Foundation
1
2
  import NitroModules
2
- import ffmpegkit
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 = ""
15
+ }
16
+ }
17
+
18
+ private let active = ActiveSession()
19
+ // Concurrent on purpose: the C core serialises executions behind its own lock,
20
+ // and a request that is waiting there can still be cancelled. A serial queue
21
+ // would hold the second call outside the core, where cancelAll() cannot see it.
22
+ private let executionQueue = DispatchQueue(
23
+ label: "com.munimtech.ffmpeg",
24
+ qos: .userInitiated,
25
+ attributes: .concurrent
26
+ )
27
+
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
+ }
3
38
 
4
39
  final class HybridMunimFfmpeg: HybridMunimFfmpegSpec {
40
+ private var sessionCounter: Double = 0
41
+
5
42
  var ffmpegVersion: String {
6
- FFmpegKitConfig.getFFmpegVersion()
43
+ String(cString: munim_ffmpeg_version())
44
+ }
45
+
46
+ private func nextSession() -> Double {
47
+ sessionCounter += 1
48
+ return sessionCounter
49
+ }
50
+
51
+ private static func temporaryFile() -> String {
52
+ FileManager.default.temporaryDirectory
53
+ .appendingPathComponent("munim-ffmpeg-\(UUID().uuidString)")
54
+ .path
55
+ }
56
+
57
+ private func result(
58
+ sessionId: Double,
59
+ returnCode: Int32,
60
+ output: String,
61
+ startedAt: Date
62
+ ) -> FFmpegSessionResult {
63
+ FFmpegSessionResult(
64
+ sessionId: sessionId,
65
+ returnCode: Double(returnCode),
66
+ success: returnCode == 0,
67
+ cancelled: returnCode == Int32(MUNIM_FFMPEG_CANCELLED),
68
+ state: "completed",
69
+ durationMs: Date().timeIntervalSince(startedAt) * 1000,
70
+ output: output,
71
+ failStackTrace: nil
72
+ )
7
73
  }
8
74
 
9
75
  func execute(
@@ -13,35 +79,38 @@ final class HybridMunimFfmpeg: HybridMunimFfmpegSpec {
13
79
  onSessionCreated: ((_ sessionId: Double) -> Void)?
14
80
  ) throws -> Promise<FFmpegSessionResult> {
15
81
  let promise = Promise<FFmpegSessionResult>()
82
+ let sessionId = nextSession()
83
+ onSessionCreated?(sessionId)
16
84
 
17
- let session = FFmpegKit.execute(
18
- withArgumentsAsync: arguments_,
19
- withCompleteCallback: { session in
20
- guard let session else {
21
- promise.reject(withError: MunimFfmpegError.missingSession)
22
- return
23
- }
24
- promise.resolve(withResult: Self.result(from: session))
25
- },
26
- withLogCallback: { log in
27
- guard let message = log?.getMessage() else { return }
28
- onLog?(message)
29
- },
30
- withStatisticsCallback: { statistics in
31
- guard let statistics else { return }
32
- onStatistics?(
33
- statistics.getTime(),
34
- Double(statistics.getSize()),
35
- statistics.getBitrate(),
36
- statistics.getSpeed(),
37
- Double(statistics.getVideoFrameNumber()),
38
- Double(statistics.getVideoFps()),
39
- Double(statistics.getVideoQuality())
40
- )
85
+ executionQueue.async {
86
+ let startedAt = Date()
87
+ // ffmpeg prints reports such as -encoders to stdout rather than through
88
+ // its logger, so it is captured to a file and appended to the output.
89
+ let printedPath = Self.temporaryFile()
90
+
91
+ installCallbacks()
92
+ active.reset()
93
+ active.onLog = onLog
94
+ active.onStatistics = onStatistics
95
+
96
+ let returnCode = withArrayOfCStrings(arguments_) { argv in
97
+ munim_ffmpeg_execute(Int32(arguments_.count), argv, printedPath)
41
98
  }
42
- )
43
- if let session {
44
- onSessionCreated?(Double(session.getId()))
99
+
100
+ let printed = (try? String(contentsOfFile: printedPath, encoding: .utf8)) ?? ""
101
+ try? FileManager.default.removeItem(atPath: printedPath)
102
+
103
+ let output = active.output + printed
104
+ active.reset()
105
+
106
+ promise.resolve(
107
+ withResult: self.result(
108
+ sessionId: sessionId,
109
+ returnCode: returnCode,
110
+ output: output,
111
+ startedAt: startedAt
112
+ )
113
+ )
45
114
  }
46
115
 
47
116
  return promise
@@ -53,23 +122,20 @@ final class HybridMunimFfmpeg: HybridMunimFfmpegSpec {
53
122
  onSessionCreated: ((_ sessionId: Double) -> Void)?
54
123
  ) throws -> Promise<FFmpegSessionResult> {
55
124
  let promise = Promise<FFmpegSessionResult>()
125
+ let sessionId = nextSession()
126
+ onSessionCreated?(sessionId)
56
127
 
57
- let session = FFprobeKit.execute(
58
- withArgumentsAsync: arguments_,
59
- withCompleteCallback: { session in
60
- guard let session else {
61
- promise.reject(withError: MunimFfmpegError.missingSession)
62
- return
63
- }
64
- promise.resolve(withResult: Self.result(from: session))
65
- },
66
- withLogCallback: { log in
67
- guard let message = log?.getMessage() else { return }
68
- onLog?(message)
69
- }
70
- )
71
- if let session {
72
- onSessionCreated?(Double(session.getId()))
128
+ executionQueue.async {
129
+ let startedAt = Date()
130
+ let (returnCode, report) = Self.runProbe(arguments_, onLog: onLog)
131
+ promise.resolve(
132
+ withResult: self.result(
133
+ sessionId: sessionId,
134
+ returnCode: returnCode,
135
+ output: report,
136
+ startedAt: startedAt
137
+ )
138
+ )
73
139
  }
74
140
 
75
141
  return promise
@@ -77,37 +143,58 @@ final class HybridMunimFfmpeg: HybridMunimFfmpegSpec {
77
143
 
78
144
  func getMediaInformation(path: String) throws -> Promise<String> {
79
145
  let promise = Promise<String>()
80
- let arguments = [
81
- "-v",
82
- "error",
83
- "-print_format",
84
- "json",
85
- "-show_format",
86
- "-show_streams",
87
- "-show_chapters",
88
- path,
89
- ]
90
-
91
- FFprobeKit.execute(
92
- withArgumentsAsync: arguments,
93
- withCompleteCallback: { session in
94
- guard let session else {
95
- promise.reject(withError: MunimFfmpegError.missingSession)
96
- return
97
- }
98
- let returnCode = session.getReturnCode()
99
- guard ReturnCode.isSuccess(returnCode) else {
100
- let message = session.getFailStackTrace() ?? session.getOutput() ?? "FFprobe failed"
101
- promise.reject(withError: MunimFfmpegError.executionFailed(message))
102
- return
103
- }
104
- promise.resolve(withResult: session.getOutput() ?? "{}")
146
+
147
+ executionQueue.async {
148
+ let (returnCode, report) = Self.runProbe(
149
+ [
150
+ "-v", "error",
151
+ "-print_format", "json",
152
+ "-show_format", "-show_streams", "-show_chapters",
153
+ path,
154
+ ],
155
+ onLog: nil
156
+ )
157
+
158
+ if returnCode != 0 {
159
+ promise.reject(
160
+ withError: MunimFfmpegError.executionFailed(
161
+ report.isEmpty ? "FFprobe failed with return code \(returnCode)" : report
162
+ )
163
+ )
164
+ return
105
165
  }
106
- )
166
+
167
+ promise.resolve(withResult: report)
168
+ }
107
169
 
108
170
  return promise
109
171
  }
110
172
 
173
+ /// ffprobe writes its report to stdout, which is not reachable from an app,
174
+ /// so it is pointed at a temporary file with `-o` and read back.
175
+ private static func runProbe(
176
+ _ arguments: [String],
177
+ onLog: ((String) -> Void)?
178
+ ) -> (Int32, String) {
179
+ let destination = temporaryFile()
180
+
181
+ installCallbacks()
182
+ active.reset()
183
+ active.onLog = onLog
184
+
185
+ let returnCode = withArrayOfCStrings(arguments) { argv in
186
+ munim_ffmpeg_probe(Int32(arguments.count), argv, destination)
187
+ }
188
+
189
+ let report = (try? String(contentsOfFile: destination, encoding: .utf8)) ?? ""
190
+ try? FileManager.default.removeItem(atPath: destination)
191
+
192
+ let logs = active.output
193
+ active.reset()
194
+
195
+ return (returnCode, report.isEmpty ? logs : report)
196
+ }
197
+
111
198
  func cancel(sessionId: Double?) throws {
112
199
  if let sessionId {
113
200
  guard
@@ -118,40 +205,39 @@ final class HybridMunimFfmpeg: HybridMunimFfmpegSpec {
118
205
  else {
119
206
  throw MunimFfmpegError.invalidSessionId(sessionId)
120
207
  }
121
- FFmpegKit.cancel(Int(sessionId))
122
- } else {
123
- FFmpegKit.cancel()
124
208
  }
209
+ // One execution runs at a time, so cancelling a specific session and
210
+ // cancelling everything are the same operation.
211
+ munim_ffmpeg_cancel()
125
212
  }
126
213
 
127
214
  func cancelAll() throws {
128
- FFmpegKit.cancel()
215
+ munim_ffmpeg_cancel()
129
216
  }
217
+ }
130
218
 
131
- private static func result(from session: Session) -> FFmpegSessionResult {
132
- let returnCode = session.getReturnCode()
133
- return FFmpegSessionResult(
134
- sessionId: Double(session.getId()),
135
- returnCode: Double(returnCode?.getValue() ?? -1),
136
- success: ReturnCode.isSuccess(returnCode),
137
- cancelled: ReturnCode.isCancel(returnCode),
138
- state: String(describing: session.getState()).lowercased(),
139
- durationMs: Double(session.getDuration()),
140
- output: session.getOutput() ?? "",
141
- failStackTrace: session.getFailStackTrace()
142
- )
219
+ /// Builds a C `argv` that stays valid for the duration of `body`.
220
+ private func withArrayOfCStrings<R>(
221
+ _ values: [String],
222
+ _ body: (UnsafePointer<UnsafePointer<CChar>?>?) -> R
223
+ ) -> R {
224
+ var pointers = values.map { strdup($0) }
225
+ defer { pointers.forEach { free($0) } }
226
+
227
+ return pointers.withUnsafeMutableBufferPointer { buffer in
228
+ buffer.baseAddress!.withMemoryRebound(
229
+ to: UnsafePointer<CChar>?.self,
230
+ capacity: buffer.count
231
+ ) { body($0) }
143
232
  }
144
233
  }
145
234
 
146
235
  private enum MunimFfmpegError: LocalizedError {
147
- case missingSession
148
236
  case executionFailed(String)
149
237
  case invalidSessionId(Double)
150
238
 
151
239
  var errorDescription: String? {
152
240
  switch self {
153
- case .missingSession:
154
- return "FFmpegKit did not return a session."
155
241
  case .executionFailed(let message):
156
242
  return message
157
243
  case .invalidSessionId(let sessionId):