I finally had a breakthrough and got my ffmpeg GUI app working. My next challenge is getting the console output to print to a text field in the app instead of to the console so that the user can monitor ffmpeg’s progress and possible errors.
In my research, I see lots of how-tos about redirecting console output to a log file, but not about redirecting it to a text field.
EDIT: I actually managed to get a version of this working using this approach. Unfortunately each new line overwrites the last so it’s not a scrolling text, but it’s a step in that direction.
Here’s my code if anyone is interested:
convertTask.arguments = arguments
convertTask.standardInput = FileHandle.nullDevice
let pipe = Pipe()
convertTask.standardError = pipe
let outHandle = pipe.fileHandleForReading
outHandle.waitForDataInBackgroundAndNotify()
var obs1 : NSObjectProtocol!
obs1 = NotificationCenter.default.addObserver(forName: NSNotification.Name.NSFileHandleDataAvailable,
object: outHandle, queue: nil) { notification -> Void in
let data = outHandle.availableData
if data.count > 0 {
if let str = NSString(data: data, encoding: String.Encoding.utf8.rawValue) {
self.ffmpegLogOutput.string = ("\(str)")
}
outHandle.waitForDataInBackgroundAndNotify()
} else {
print("EOF on stderr from process")
NotificationCenter.default.removeObserver(obs1!)
}
}
var obs2 : NSObjectProtocol!
obs2 = NotificationCenter.default.addObserver(forName: Process.didTerminateNotification,
object: convertTask, queue: nil) { notification -> Void in
print("terminated")
NotificationCenter.default.removeObserver(obs2!)
}
convertTask.launch()
convertTask.waitUntilExit()
}
@ncrusher Do you still have issues with this?
Someone from the Swift forums helped me build a better what. Here is what I ended up with:
/// The pipe that will receive standard out and standard error.
let pipe = Pipe()
// Hook up the pipe.
process.standardOutput = pipe
process.standardError = pipe
/// Starts the external process.
// (This does the same as ”launch()“, but it is a more modern API.)
try process.run()
/// A variable to store the output as we catch it.
var output = String()
/// A buffer to store partial lines of data before we can turn them into strings.
///
/// This is important, because we might get only the first or the second half of a multi‐byte character. Converting it before getting the rest of the character would result in corrupt replacements (�).
var stream = Data()
/// The POSIX newline character. (Windows line endings are more complicated, but they still contain it.)
let newline = "\n"
/// The newline as a single byte of data instead of a string (since we will be looking for it in data, not strings.
let newlineData = newline.data(using: String.Encoding.utf8)!
/// Reads whatever data the pipe has ready for us so far, returning `nil` when the pipe finished.
func read() -> Data? {
/// The data in the pipe.
///
/// This will only be empty if the pipe is finished. Otherwise the pipe will stall until it has more. (See the documentation for `availableData`.)
let newData = pipe.fileHandleForReading.availableData
// If the new data is empty, the pipe was indicating that it is finished. `nil` is a better indicator of that, so we return `nil`.
// If there actually is data, we return it.
return newData.isEmpty ? nil : newData
}
// The purpose in the following loop‐based design is two‐fold:
// 1. Each line of output can be handled in real time as it arrives.
// 2. The entire thing is encapsulated in a single synchronous function. (It is much easier to send a synchronous task to another queue than it is to stall a queue to wait for the result of an asynchronous task.)
/// Whether the loop should end. We will set it to `true` when we want it to stop.
var shouldEnd = false
// Loop as long as “shouldEnd” is “false”.
while !shouldEnd {
// “autoreleasepool” cleans up after some Objective C stuff Foundation will be doing.
autoreleasepool {
guard let newData = read() else {
// “read()” returned `nil`, so we reached the end.
shouldEnd = true
// This returns from the autorelease pool.
return
}
// Put the new data into the buffer.
stream.append(newData)
// Loop as long as we can find a line break in what’s left.
while let lineEnd = stream.range(of: newlineData) {
/// The data up to the newline.
let lineData = stream.subdata(in: stream.startIndex ..< lineEnd.lowerBound)
// Clear the part of the buffer we’ve already dealt with.
stream.removeSubrange(..<lineEnd.upperBound)
/// The line converted back to a string.
let line = String(decoding: lineData, as: UTF8.self)
output += line+newline
}
}
}
/// Stall until the external process is done, even if it closed its pipes early.
while process.isRunning {}
/// Check whether it ended with success or an error.
if process.terminationStatus == 0 {
// It succeeded.
// Return the output.
return output
} else {
// It failed.
throw ExternalProcessError.processErrored(
exitCode: Int(process.terminationStatus),
output: output)
}
}
Hi @ncrusher,
Did it work for you?
cheers,