Skip to main content
A reactor::Track is a handle onto one named media slot the model declared — not something you construct yourself. Ask for it by name with client.track(name), or find it by filtering client.tracks() when you don’t know the name:
One type covers both directions and both kinds, because the operations are the same operations either way. There is no push_video_frame / push_audio_frame split and no on_video_frame / on_audio_frame split at the class level: the track already knows its kind, and the method you call says which you meant.
A handle, not an owner: it holds the client weakly, so a track parked in a capture thread cannot keep the session — and the native handle — alive for the life of that thread. Using one after the client is gone throws InvalidStateError.
Calling a method the track’s kind or direction does not allow throws, on purpose: push_frame() on a recvonly track, on_frame() on a sendonly one, pause() on a sendonly track. Each of those would otherwise reach the native layer, find nothing to do, and return — so a caller pushing at 30fps would see a model receiving nothing and no reason why.

Properties

name()

Signature
The declared name. Never changes.

kind()

Signature
TrackKind::Video or TrackKind::Audio, or empty before the session has declared its tracks.

direction()

Signature
TrackDirection::SendOnly or TrackDirection::RecvOnly, or empty before the session has declared them. SendOnly is from this client’s point of view: this client sends, the model receives.

mid()

Signature
The SDP media id, once the track has been received. Read from the client rather than remembered here: it is reported as tracks arrive and is renegotiated on a reconnect.

paused()

Signature
Whether this track is paused right now. Read from the session, not cached, so it stays right across a reconnect — recvonly tracks resume automatically once connected, and a Track holding a stale true would go on claiming otherwise.

published()

Signature
Whether this sendonly slot is activated. Kept by the SDK rather than read back, because the session does not record it: publish is a control request and unpublish a notification, and neither leaves anything to query.
It is cleared whenever the status leaves Ready. A reconnect resumes recvonly tracks and nothing else, so a slot published before one is not published after it — publish again. See reconnect().

Sending

publish()

Activates this sendonly slot, so the model has something to receive on.
Signature
Publishing is what puts a sender behind the slot: pushing before it would drop the frame, and this SDK refuses rather than letting it. Throws on a recvonly track, and on a session that is not Ready.
Example

unpublish()

Deactivates the slot. Synchronous, unlike the other track methods — there is no round trip, only a local state change and a fire-and-forget notification.
Signature
Throws when the notification could not be made; the track then stays published, so a retry is possible.

push_frame()

Pushes one BGRA frame into this track.
Signature
Bytes
required
Exactly width * height * 4 bytes: B, G, R, A per pixel. Checked here, because the native layer reads what it is told to read and a wrong length is a read past the end of your buffer.
std::uint32_t
required
Frame width in pixels.
std::uint32_t
required
Frame height in pixels.
FrameOptions
Metadata and capture time — see below.
Example
Throws InvalidStateError on a recvonly track, before publish(), or once the session has left Ready; BadRequestError on a buffer whose length does not match the dimensions.

FrameOptions

Definition
Bytes
Bytes the far end reads as this frame’s metadata. Sent as-is — JSON, protobuf or anything else is between you and the model — and dropped silently by a peer that did not declare it reads them, so tagging is safe whatever the far end supports. See Frame Metadata.
std::optional<std::int64_t>
When this frame was captured, read from reactor::time_micros(). Left empty, the frame is stamped as it is pushed, so several tracks capturing one moment arrive microseconds apart.
Example

push_audio()

Pushes interleaved 16-bit PCM into this track.
Signature
sample_rate must be 48000 and channels 1, which is what the source expects; pcm.size must divide evenly by channels.
Example

set_bitrate()

Bounds what this one sender may spend, in bits per second.
Signature
Definition
This is the ceiling that actually caps a video encoder, and it is easy to hit without knowing it exists: with nothing set, WebRTC derives a sender’s maximum from the frame size alone, and that maximum is 2500 kbps for anything above 960x540. 720p, 1080p and 4K all cap at 2.5 Mbps.
Example
Reactor::set_bitrate() is the other ceiling — the whole connection’s budget. The two are conjunctive, so raising only one changes nothing.A ceiling is permission, not a target: the encoder still spends only what the congestion controller allocated and what the picture needs. What raising it buys is headroom for the moments that would otherwise clip.
Throws on a recvonly track — the sender behind an incoming track is the far end’s, and nothing here can bound it — and on a session that is not Ready.

Receiving

on_frame()

Receives decoded video frames from this track.
Signature
Example
Only this track’s frames reach it — every media handler in this SDK is scoped to one track. Throws on a sendonly track (the callback would never fire, which is indistinguishable from a model that sends nothing) and on an audio track, which has on_audio().
Hold the returned Subscription. Discarding it unregisters the handler immediately.

on_audio()

Receives decoded audio frames. Refuses the wrong kind or direction, as on_frame() does.
Signature
Example

VideoFrame

Definition
std::string_view
The track this arrived on. Every recvonly video track decodes into one callback, so on a session with several this is what tells them apart.
const std::uint8_t*
BGRA pixels: width * height * 4 bytes, which size_bytes() returns.
std::uint64_t
The sender’s frame counter, or 0 when the frame carried no metadata trailer.
std::uint64_t
The sender’s capture time in microseconds, or 0 with no trailer. Read in the engine’s clock (time_micros()), not the system’s.
Bytes
Whatever the sender tagged this frame with — bytes, not text. Empty when the frame carried no trailer, which is the normal case for a model that does not tag.
The pixels are borrowed for the duration of the handler and no longer. The library frees them when the handler returns, so anything you keep has to be copied — a pointer stored here is a use-after-free that reproduces under load and not in tests. The same goes for user_data and track_name.
The handler runs inline on the library’s delivery thread, deliberately. Blocking in it is the backpressure: while it runs, the library keeps only the newest frame and drops the ones in between. Handing frames to a queue of your own trades a bounded drop for unbounded latency and memory.This is why media handlers do not go through Options::executor, and control-event handlers do.

AudioFrame

Definition
Borrowed and inline, exactly as VideoFrame. The audio queue is short and keeps its backlog rather than dropping, because there the queue is the jitter buffer and a hole in it is audible — so a slow handler here costs latency instead of frames. frames() is samples per channel, which is what a playback device asks for.

Pausing

pause()

Signature
Stops this track. Nothing is generated while paused, which on a video track is visible only as a frozen frame.

resume()

Signature
Starts it again.

TrackList

The tracks a session declared, filterable — for discovery, and for a caller who would rather not hardcode a name. Returned by client.tracks().
Signature
Filters chain in either order, and it iterates:
Example
one() throws NotFoundError when the list is empty and InvalidStateError when it holds more than one — a filter that matched several and a caller that wanted one is a question with no answer, and picking the first would answer it wrongly and silently.

TrackKind and TrackDirection

Definition
Both convert to and from their wire spellings:
Signature
The parsers return empty for a value this build does not recognise, rather than guessing.