The player's streaming output: the same muxer, with the target changed to a line of address

Chapter 7: Recording: frame sources, software encoding, muxing and streaming·about 1205 words

Recording and streaming differ only in that output line of address

The recorder's interface has just two parameters: source address, output address. Write a file name and it is recording; write rtmp:// and it is streaming. The recorder itself does not recognize protocols; it hands the second line of address to the muxing implementation as is, and that side decides how to open it.

So “streaming” on this chain is not a different class and not a switch: the same muxing implementation, the same flow — open the output, write the header, interleaved-write packet by packet, write the trailer. The write thread, access-unit merging and the two timestamp steps from the muxing article all run again for streaming.

What changes is where the format comes from and how the target is opened.

The outside surface is only settings and queries, all before open

The recorder's settings all take effect before open: how the source is obtained and how the output is muxed (both default to ffmpeg if unset), which codecs video and audio use, and how each of the two tracks is handled (which track is re-encoded, which is copied directly).

The muxing implementation itself also goes through the registry: besides the default ffmpeg-based one, there is another based on zlmediakit, using its own muxing and streaming. What is swapped is the muxing segment — the recorder's entry, queues and encoding do not move.

Setting the codec is also the step where something is given up: set one side's codec to “none” and that track is dropped — that is how video-only or audio-only streaming works.

After open the chain runs on its own: when the source is read out, or an IO error occurs, it winds down and closes itself without the layer above having to close it; to stop it on purpose, call close once. To find out progress in between, just ask: state, progress, the source's total duration — a live stream cannot give a total duration, which is also why a live stream cannot seek.

An attached observer receives five callbacks: state change, progress, IO error, encode error, complete. The two error kinds are reported separately: the IO one is about reads, writes and connections, the encode one is that some track can no longer encode (the callback carries which track). The processed-duration field in progress may take the value “not measured in time”, which is how a source that cannot give a time expresses it.

Output format: guess from the suffix first, map from the protocol if that fails

How a line of address becomes a muxing format: guess from the suffix first, map the protocol to a format name when there is no suffix

The muxing implementation has only that line of address, and derives the format from it. First it guesses from the extension.

After the guess there are two places to correct. One is that mp4 cannot hold non-AAC audio, in which case it switches to mov automatically — the container changes, the codecs and streams do not. The other is an address with no extension at all, which protocol addresses usually lack; then the part before :// is looked up as a muxing format name. Two names do not match here: the rtmp family actually carries flv data, and there is no muxing format called rtmp in the library, so it has to map to flv; rtsps has to map to rtsp.

The rtsp side adds two more parameters: transport is forced to TCP, because UDP loses packets easily across network segments, and enough loss to hit the timeout means a disconnect; sending carries a low-latency option.

Opening the output makes no distinction between local and network: one open function — give it a file name and it opens a file, give it rtmp:// and it builds a connection. Only muxing formats that manage their own connection (marked “no file”) skip this step — they take the address and connect themselves.

Handling failures at stream start and on the write

Two levels of write-failure handling: shut down when the output is broken, drop only the frame when the problem is that one frame

Opening the output and writing the header are both handled on the spot on failure: release the output context, return failure, and do not retry afterwards. A header-write failure also marks the chain as initialization-failed, and the write thread exits as soon as it sees the mark — it will not stuff packets into a container whose header was never written.

The write is still the interleaved write described in the muxing article, and its failures come in two levels.

One level is the output itself being broken: connection lost, peer closed, disk full, bad handle, IO error. This kind reports the error and shuts down, releasing the output context and writing no more — keeping a broken context around makes it jump up and crash when the trailer is written during shutdown.

The other level is a problem with the frame itself, for example a decode timestamp that is not monotonically increasing, which the container rejects. This kind drops just this frame and keeps writing the rest: the first one logs a line, after that only a count is kept. The reason is practical — one seek, or jittery timestamps on a stream, can make a hundred-odd packets in a row out of order, and logging each one would flood the log.

Landing on disk has one more switch: the muxing library by default accumulates a batch of bytes and lands them only on close; this chain turns that switch on, so a use case that pulls and converts on the fly can see the product immediately. If the peer disconnects it does not retry on its own: the chain winds down, and to keep streaming you have to open it once more.

How fast streaming goes: the source and the write speed

There is no wall-clock throttling anywhere on this chain: the write thread takes packets from the queue, a full queue blocks the enqueue (that queue is described in the muxing article), and the pressure travels all the way back to the source. So the tempo is set by the slowest end — with a live source, the slow end is usually the write speed; with a file source, the slow end is how fast writing can go, and in that case the chain does not push slowly to match the content's original duration.

When the peer writes slowly, that pressure travels all the way back to the source side: the output queue fills up, encoding stops, decoding stops, and reading stops with them — memory does not pile up just because streaming is slow.

For the same content, with a file source the wall-clock time spent sending is shorter than the content's own duration, and the bandwidth taken does not change because sending is fast. To make streaming follow the content's own duration, you have to throttle at the source — this chain's tempo is given only by those two ends.

Here the chain is complete: frame sources, frames to packets, packets to a file, and the target changed to a line of address (this article). What changes further down is the encoding side — each platform replaces this software encoder with its own hardware encoder, while the muxing and output segment stays put.

The avox engine is open source on GitHub: github.com/xxxzhou/avox. This recording chain, the muxing layer and the output-target segment are all in there.