Hathor Logo

Week 2: Breadboard 1.0 Done

This entry is part 6 of 7 in the series The Streaming Chronicles

As I mentioned earlier this week, I decided to set up something that is more of an “ffmpeg test harness,” than a “shipping” server project.

I wanted to have an app that would allow me to do all the command-line stuff that you can do with ffmpeg, but also allow direct, intravenous access to the guts of a Swift project, so I could monkey with a bunch of stuff that might not be so easily done (or possible at all) with Bash.

The RVS_PeristentPrefs stuff was a big success. Because of it, it was a breeze to add a number of persistent states to the app to access special tasks; namely, establishing a “special access” mode, and a console display in the server screen.

Here’s the tag for this week’s work.

I also took the opportunity to refactor the project a bit more, to make it a bit easier to grok and maintain. I broke the server handlers out of the View Controller for the server screen, and put them into their own class. Not 100% MVVM, but close enough. Most of the model is bound to the UI by KVO anyway, so the ViewControllers are titchy.

The biggest issue that I had, and I’m embarrassed to admit that it took me a day to figure out, was the best way to pipe the text that ffmpeg emits while it’s running, into a console screen.

First, I had to figure out the best way to pipe the output.

General consensus seemed to coalesce around setting up a notification handler, and assigning it to the stdout. I’ll cover that in a minute.

stdout is a standard FreeBSD UNIX (the ultimate ancestor of the Apple operating systems) command-line utility and library function. It’s how all that stuff shows up on the console. stderr is the same thing, but as a separate channel, so utilities can output error information separately, and users of the utility can make smarter decisions.

stdout covers a lot. It isn’t just text dumps. It can be massive, continuous streams of binary data. Think of these streams as big fat pipes with universal coupling, so they can be attached to all kinds of applications.

ffmpeg doesn’t send status text to the standard output. I assume that’s because it reserves stdout for actual video data. Instead, it uses stderr.

So I spent a whole lot of time, bashing my head against a wall, trying to figure out why I never got anything from stdout. Once I hooked up stderr, everything came out the way it should.

Setting Up A Notification Handler

Before we get started, here’s a link to the actual code for where I set up the notification. I’ll show it to you as I get into it.

There’s a bunch of ways to intercept standard output/error from a running process, but the one that most folks seem to like involves setting up a standard NotificationCenter observer. This gets called whenever new text appears in the pipe being observed. This seemed a great way to get the ffmpeg status output, and send it to the console display.

So this is the code that I implemented to intercept the ffmpeg output, and send it to the server display:

/* ################################################################## */
/**
 This sets up a trap, so we can intercept the textual output from ffmpeg (which comes out on stderr).
 
 - parameter inTask: The ffmpeg task we're intercepting.
 */
func openErrorPipe(_ inTask: Process) {
    stderrPipe = Pipe()
    // This closure will intercept stderr from the input task.
    stdErrObserver = NotificationCenter.default.addObserver(forName: .NSFileHandleDataAvailable, object: stderrPipe.fileHandleForReading, queue: nil) { [unowned self] _ in
        let data = self.stderrPipe.fileHandleForReading.availableData
        if 0 < data.count {
            let str = String(data: data, encoding: .ascii) ?? "\n"
            if let delegate = self.delegate {   // If we have a delegate, then we call it.
                // We call delegate methods in the main thread.
                DispatchQueue.main.async {
                    delegate.mediaServerManager(self, ffmpegConsoleTextReceived: str)
                }
            } else {
                print(str)  // Otherwise, just print to the console.
            }
            self.stderrPipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
        } else if let stdErrObserver = self.stdErrObserver {
            NotificationCenter.default.removeObserver(stdErrObserver)
            self.stdErrObserver = nil
        }
    }
    
    inTask.standardError = stderrPipe
    self.stderrPipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
}

stderrPipe is an instance property (an “owned” property of the class instance), so it is available outside the method. stdErrObserver is also an instance property that we will use to hold the observer handler.

Since we are only interested in ffmpeg, we will only look at stderr.

Delegation

Note the way that we handle sending the text to the console display. I am using a fairly standard delegation pattern. Yeah, it’s a bit “clunky,” compared to some of the ways that people like to do stuff, but I didn’t want to get fancy for such a basic operation. Delegate works fine. If I find I need to get fancier, I can, but this is what I’m doing now.

Here’s the delegate definition:

/* ################################################################################################################################## */
// MARK: - Delegate Protocol
/* ################################################################################################################################## */
/**
 These are methods that can be called from the manager to a registered delegate.
 
 They are all called on the main thread, and are all optional.
 */
protocol RVS_MediaServer_ServerManagerDelegate: class {
    /* ################################################################## */
    /**
     Called to deliver text intercepted from ffmpeg.
     
     - parameter manager: The manager object
     - parameter ffmpegConsoleTextReceived: The text received.
     */
    func mediaServerManager( _ manager: RVS_MediaServer_ServerManager, ffmpegConsoleTextReceived: String)
}

/* ################################################################################################################################## */
// MARK: - Delegate Protocol Extension
/* ################################################################################################################################## */
/**
 This is an extension that allows the protocol methods to be optional.
 
 They do nothing.
 */
extension RVS_MediaServer_ServerManagerDelegate {
    /* ################################################################## */
    /**
     Does Nothing.
     
     - parameter: ignored
     - parameter ffmpegConsoleTextReceived: ignored.
     */
    func mediaServerManager( _: RVS_MediaServer_ServerManager, ffmpegConsoleTextReceived: String) { }
}

Note that I provide an extension, with a default “do nothing” implementation of the (so far) single method. That makes the method optional. Implementors don’t need to add the method, just to satisfy the protocol.

The reason for the delegate, is that it’s not a good programming practice to send data out from the handler. It should be requested, and channels should be required by the requestor. This ensures that the handler class doesn’t need to know about anything more than the classes it’s working with. It publishes the delegate, which says “I got good stuff for you. If you want it, send me a self-addressed, stamped envelope.”

In order to get the data, the view controller that manages the console display registers as a delegate of the handler, and will receive the text as it is read.

Also note that the delegate call is surrounded by DispatchQueue.main.async {...}. This is because anything that interacts with user interface needs to happen on the main thread. By sending this in the main thread, the consumer doesn’t need to worry about whether or not the data they get will need to be added in a dispatcher. If you are a “purist,” then you leave it up to the consumer to do that, but I find that it’s usually a better idea for me to do it in the model, if possible. Thread bugs are very strange, and can be difficult to track down.

Bringing It All Together

So the structure I chose was to have the command-line entry done in the preferences screen, and the output text displayed in the running server screen. They can both be open, are resizable, and could even be placed in separate monitors, if you have a multi-monitor system.

Here’s what it looks like now:

The Server Window With No Server Running
The Basic Preferences Screen for A Simple HTTP Server

With the Console Window Open
With the Command-Line Entry Open
With the Server Running
Without An HTTP Server

So now we have something that I can use to start testing various ffmpeg configurations, as well as being able to write code directly that interacts with ffmpeg.

I’m quite sure that it will be getting a lot more complex, as time goes on, but this is a good starting point.