^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
DEVELOPERS' WORKSHOP: Muxamania Part 2: Better Than Haggis
  (The Guts of SelectorNode)
By Owen Smith -- <orpheus@be.com>
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

In the previous installment, I introduced a node called
SelectorNode. This node allows you to select one of several
inputs to pass to a single output. This week, I'd like to
explore the guts of the node in a little more detail.

The sample code (plus some scrubbing and general clean-up
from last week's effort) can again be found at

<ftp://ftp.be.com/pub/samples/media_kit/Selector.zip>

I'd like to answer two questions about timing that I
encountered whilst wrangling with the selector node for
last week's diatribe. They are

* When should I send a buffer that I've received?

* How can I inform my upstream nodes when my latency
  changes?

Introducing... BMediaEventLooper!

The node looks vastly different from previous nodes I've
presented because it derives from the super-nifty and newly
christened BMediaEventLooper (formerly known as the Mother
Node, and featured in Stephen's article two weeks ago) to
manage the timing of buffers and events. Previously, timing
was the trickiest part about developing nodes. Now, it takes
little more than a few method calls, and leaves you with
almost no steaming entrails. What does BMediaEventLooper
give you?

* You no longer have to write a Big Bad Service Thread to
  receive and process messages on your port. The BMediaEvent
  Looper creates and maintains the service thread and control
  port for you (thus, very similar to a BLooper in function).

* You no longer have to worry about holding on to events
  until it's time to do them (another feature of the Big Bad
  Service Threads of the past). The BMediaEventLooper
  maintains a BTimedEventQueue. You simply push events onto
  the queue as you receive them, and the BMediaEventLooper
  simply pops events off the queue when they become due.

* If all your node wants is to know when Start, Stop, or
  Seek requests become due, your node doesn't have to override
  Start, Stop, or Seek anymore. The BMediaEventLooper
  automatically intercepts these events and pushes them onto
  the queue for you.

* Your node doesn't have to be limited to storing one
  Start, Stop, or Seek at a time. The BTimedEventQueue allows
  you to stack up as many events as your heart desires. (Can
  you say "automation?" I knew you could.)

* You can and should use BMediaEventLooper::EventLatency()
  to store the total latency of your node (that is, processing
  plus downstream latency). BMediaEventLooper takes this
  latency into account when it determines when to pop events
  off of the queue.

You'll see the BMediaEventLooper in action when I show you
how SelectorNode handles buffers.

Handling the Buffer Binge

Thanks to BMediaEventLooper, most of the trickiness in
getting buffers handled at the proper time is done for you.
All SelectorNode has to do is filter the incoming buffers
so that only the selected input's buffers get through. What
we have to determine is: when should SelectorNode filter
and send the buffers that it receives?

The easiest approach would be to filter buffers as soon as
they are received, and send them on their merry way as soon
as possible, as the following diagram illustrates:

Buffer Received and Handled                   Buffer Is Late
|                                                          |
V                                                          V
|OOOOOO|---------------------------------------------------|

Legend:
|OO| = time spent by processing the buffer
---- = time spent by waiting

For such a simple filter as Selector, this would be a
passable approach. However, a more robust node will take
pains not to deal with the buffer too early. Why? Consider
the pathological case where we receive a Stop, or are told
to change the selected input, between the time we receive
the buffer (if it was sent super-early) and the time by
which the buffer has to be sent. If we have already sent
the early buffer, then that buffer will escape whatever
commands we might otherwise want to apply that will affect
the buffer.

Another way to manage buffers would be to handle and send
them as late as possible so that they'll still arrive on
time, taking only the amount of time you need to process the
buffer into account:

Buffer Received                               Buffer Is Late
|                                          Buffer Handled  |
|                                                   |      |
V                                                   V      V
|---------------------------------------------------|OOOOOO|

This will take care of the race condition between commands
and early buffers. However, because you've eliminated all of
the slack time you might otherwise be able to use to process
buffers, your node becomes susceptible to jitter from
factors such as CPU load and disk activity, and your buffers
stand a dangerous chance of arriving late.

The best approach in this case is to strike a compromise.
Let's take raw audio and video into account, so that we
can calculate how long our buffers will be. In this case,
we should try to handle the buffer as soon as possible,
*but not earlier than* the duration of one buffer before
the time at which the buffer must be sent:

Buffer Received             Buffer Handled    Buffer Is Late
|                             |                            |
V                             V                            V
|-----------------------------|OOOOOO|---------------------|
                              <---------------------------->
                                       Buffer Duration

This will ensure that we properly handle commands that apply
to our buffer, but it will give us enough room to handle
unpredictable delays in processing.

Fortunately, with the BMediaEventLooper's event queue, doing
this the right way is a snap. There are three simple steps:

* We report our processing latency as being the duration of
  one buffer, plus whatever our estimated scheduling jitter
  is for our thread.

* We calculate our total latency by adding this processing
  latency to the latency downstream.

* Finally, we tell the BMediaEventLooper what our total
  latency is by calling SetEventLatency(), so that it pops
  events at the proper time.

Here's what it looks like in code:

void
SelectorNode::Connect(..., media_format& with, ...)
{
    ...

    // Calculate the processing latency.
    bigtime_t proc = buffer_duration(with);
    proc += estimate_max_scheduling_latency();

    // Calculate the downstream latency.
    bigtime_t downstream;
    media_node_id ts;
    FindLatencyFor(m_output.destination, &downstream, &ts);

    bigtime_t totalLatency = proc + downstream;

    // Tell the event queue what our new latency is.
    SetEventLatency(totalLatency);
    ...
}

Now, all we need to do is push buffers onto the queue as we
receive them. At the proper time, they'll be popped from
the queue and handed to HandleEvent, which we override to
actually send the buffer.

void
SelectorNode::BufferReceived(BBuffer *b)
{
    // The B_RECYCLE constant means that any buffers
    // inadvertently left in the queue will be recycled
    // when the queue is deleted.
    EventQueue()->PushEvent(b->Header()->start_time,
        BTimedEventQueue::B_HANDLE_BUFFER, b,
        BTimedEventQueue::B_RECYCLE, 0);
}

void
SelectorNode::HandleEvent(bigtime_t performance_time,
    int32 what, const void *pointer, uint32 cleanup,
    int64 data)
{
    switch (what) {
    case BTimedEventQueue::B_HANDLE_BUFFER:
        {
            // It's time to handle this buffer.
            BBuffer* b = (BBuffer*) pointer;

            if (b->Header()->destination
                != (uint32) m_selectedInput)
            {
                // This buffer doesn't belong to the
                // selected input, so get rid of it!
                b->Recycle();
                break;
			}

            // This buffer does belong to our selected
            // input, so try to send it.
            if ((! IsConnected()) || IsStopped()
                || (! m_enabled) // output enabled
                || SendBuffer(b, m_output.destination)
                    != B_OK)
            {
                // Either we shouldn't send the buffer
                // or the send failed. In either case,
                // get rid of the buffer.
                b->Recycle();
            }
        }
        break;
    ...
    }
}

This is a decent approach for the slackers out there like
myself. However, the astute observer will note one
disadvantage to this approach: your node's latency is
increased to one whole buffer's duration. Stack a few of
these nodes together in a chain, and you end up with far
more latency than you really need. So, for you bean counters
out there, what you really want is to report your standard
processing latency when handling events as usual, *but*
instruct the BMediaEventLooper to pop buffers a buffer's
duration early if it can. We're working on building this
sophistication into BMediaEventLooper right now, to show up
in a future release. Stay tuned, O true believers...

Captain Hook: Managing the Format and Connections

Another interesting part of the SelectorNode deals with
negotiating the format and the connections.

As you can see from the above, the selector node passes
buffers blindly, so it must enforce that the output
connection have the same format as the input connections.
It does this by rejecting all output connections until its
first input is connected, and then uses the first input's
connection format as the non-negotiable format for all
future inputs and outputs.

Because I'm connecting upstream nodes before downstream
nodes in this case, there's an additional complication
that gets introduced. When I begin, the node chain for my
application looks something like this, including the
approximate processing latency for the downstream nodes:

File Reader             Selector Node                 Output
                         latency=1ms            latency=50ms

The first connection is made between the file reader and the
selector node. Once this is done, the file reader will see a
downstream latency of 1ms, because only the selector node is
currently downstream.

File Reader ----------> Selector Node                 Output
                         latency=1ms            latency=50ms
           <------------------------>
             downstream latency=1ms

The next connection is made between the file reader and the
selector node:

File Reader ----------> Selector Node --------------> Output
                         latency=1ms            latency=50ms
           <----------------------------------------------->
                        downstream latency=51ms

Because the Output is now connected, the downstream latency
for the File Reader has just increased tremendously. But
there is currently no good mechanism for the selector node
to tell the File Reader that its latency has just changed,
so the File Reader still thinks its latency is only 1ms.
The result? All the file reader's buffers end up arriving
50ms late!

We've solved this problem by adding two simple functions to
the Media Kit API for genki/5; you can see these functions
in action in SelectorNode:

* BBufferConsumer::SendLatencyChange() informs an upstream
  producer what the new downstream latency from us is.

* BBufferProducer::LatencyChanged() is the corresponding
  function that's called on the upstream producer. The
  producer then makes whatever adjustments are necessary to
  abide by the new latency.

That's it for this week's installment. Hopefully, this week
I've managed to give you a taste of stuff we're adding to
the Media Kit to make the node writer's life easier. As
always, let us know what else we can do to pave the road
for you. And have fun developing those tractor nodes!
