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.
- package/NitroMunimFfmpeg.podspec +15 -9
- package/README.md +180 -35
- package/android/build.gradle +2 -2
- package/android/src/main/java/com/margelo/nitro/munimffmpeg/FFmpegNative.kt +86 -0
- package/android/src/main/java/com/margelo/nitro/munimffmpeg/HybridMunimFfmpeg.kt +100 -63
- package/app.plugin.js +58 -2
- package/ios/Bridge.h +4 -1
- package/ios/HybridMunimFfmpeg.swift +176 -90
- package/ios/munim_ffmpeg_core.h +58 -0
- package/lib/index.d.ts +23 -0
- package/lib/index.js +73 -3
- package/package.json +36 -38
- package/scripts/binaries.json +5 -0
- package/scripts/fetch-binaries.mjs +115 -0
- package/src/index.ts +94 -3
- package/scripts/patch-ffmpegkit-level.rb +0 -45
|
@@ -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() =
|
|
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
|
|
52
|
+
val sessionId = nextSession()
|
|
53
|
+
onSessionCreated?.invoke(sessionId)
|
|
33
54
|
|
|
34
|
-
|
|
35
|
-
val
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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.
|
|
42
|
-
statistics.
|
|
43
|
-
statistics.
|
|
69
|
+
statistics.timeMs,
|
|
70
|
+
statistics.sizeBytes,
|
|
71
|
+
statistics.bitrateKbits,
|
|
44
72
|
statistics.speed,
|
|
45
|
-
statistics.videoFrameNumber
|
|
46
|
-
statistics.
|
|
47
|
-
statistics.
|
|
73
|
+
statistics.videoFrameNumber,
|
|
74
|
+
statistics.fps,
|
|
75
|
+
statistics.quality,
|
|
48
76
|
)
|
|
49
77
|
},
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
|
95
|
+
val sessionId = nextSession()
|
|
96
|
+
onSessionCreated?.invoke(sessionId)
|
|
65
97
|
|
|
66
|
-
|
|
67
|
-
val
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
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 (
|
|
121
|
+
if (returnCode != 0) {
|
|
97
122
|
throw IllegalStateException(
|
|
98
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
215
|
+
munim_ffmpeg_cancel()
|
|
129
216
|
}
|
|
217
|
+
}
|
|
130
218
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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):
|