Every C++ programmer understands that taming and writing elegant code for complex C++ applications is no easy task. Herb Sutter himself said that C++ gives you just enough rope to hang yourself, which is a great insight into the power and nuances of the language - and if it were true, then C++ would take many lifetimes to master! C++11 brings us a little closer, but of course the real implementation is left to the programmer.
One particular class that’s used extensively throughout LibSourcey to simplify data processing is the
PacketStream acts as a delegate chain for any class that handles and emits the
IPacket type, allowing any processor to dynamically plug into the stream and preform arbitrary processing on packets as they are passed down the stream.
Take a look at the pseudo code below which takes a video device input, applies a motion filter to it, encodes the filtered video as H.264, and broadcasts the encoded media to delegates listening in on the output
// create the stream PacketStream stream; // init and attach capture VideoCapture video(0); AudioCapture audio(0, 2, 44100); stream.attach(video, false); stream.attach(audio, false); // init and attach raw video processor auto detector = new MotionDetector; detector->setVideoCapture(video); attach(detector, 1, true); // init and attach encoder EncoderParams params; params.oformat = Format("MP4", Format::MP4, VideoCodec(Codec::H264, "H264", 400, 300, 25), AudioCodec(Codec::AAC, "AAC", 2, 44100)); auto encoder = new AVEncoder(params); encoder->initialize(); stream.attach(encoder, 5, true); // other processors may be attached here for // processing media data emitted by the encoder. // start the stream and encoding stream.start();
Pretty neat huh? The
PacketStream can also be forked so each output stream can be processed independently. This might be useful if we want to take a single encoder output, and packetise the data separately for broadcasting to multiple destinations:
// main stream PacketStream stream; // attach processors to the main stream... // forked stream 1 PacketStream fork1; // attach processors or packetisers to fork1... // attach main stream to fork1 fork1->attach(&stream, false, false); // forked stream 2 PacketStream fork2; // attach processors or packetisers to fork2... // attach main stream to fork2 fork2->attach(&stream, false, false); // start pumping data into the delegate chain stream.start();
As you can see this method of processing data is quite flexible. By default the
PacketStream doesn’t copy any data, unless an internal
PacketQueue is employed to buffer and synchronise output packets with different threads, or simply defer processing from the source thread.
Most LibSourcey classes that handle packets have a
PacketAdapter implementation which allows them to interface with a
PacketStream. We find that using this model helps us adhere to C++ best practices and write better code by keeping classes simple and modular.