blob: 8ac6217c6a5f09a4cf11232e491d4603212b2baf [file] [view]
# Cast Streaming
This module contains an implementation of Cast Streaming, the real-time media
streaming protocol between Cast senders and Cast receivers.
[TOC]
## Directory Structure
The `streaming` directory is organized as follows:
- `public/`: Contains the public C++ header files that form the primary API for
the streaming library.
- `impl/`: Contains the internal implementation details of the streaming
protocol. The components in this directory are not meant to be used directly
by most applications.
- `testing/`: Contains testing-related utilities and mocks.
- The root directory contains build files and protocol buffer definitions, as
well as some implementation and public specific files, although this is
discouraged in favor of using `public/` and `impl/` where possible.
## API Overview
This document provides a code-level overview of the Cast Streaming Sender and
Receiver APIs. For a higher-level view of the entire Cast ecosystem, please see
the main [architecture document](../../cast/docs/architecture.md).
The Cast Streaming library is designed around two primary classes: the `Sender`
and the `Receiver`.
### Core Concepts: Sender and Receiver
- [**`cast::Sender`**](./sender.h): This class is used by the client application
that wishes to stream media _to_ a Cast device. Its main responsibility is to
take encoded media frames (e.g., from a video encoder), packetize them into
RTP packets, and send them to a `Receiver`. It also handles feedback from the
`Receiver`, such as acknowledgements and requests for retransmissions.
- [**`cast::Receiver`**](./receiver.h): This class runs on the Cast device that
is the target for the media stream. It receives RTP packets, reassembles them
into encoded media frames, and makes them available to a client application
(e.g., a media player) for decoding and rendering.
A typical Cast session involves at least one `Sender` and `Receiver` pair. For
A/V streaming, there are usually two pairs: one for audio and one for video.
### The Offer/Answer Negotiation Flow
Before streaming can begin, the `Sender` and `Receiver` must negotiate the
parameters of the stream. This is done using an "Offer/Answer" model, similar to
WebRTC.
1. **The Sender Creates an `Offer`**: The sender application constructs an
`Offer` object. This object describes the media stream(s) the sender wishes
to send, including details like:
- Codecs (e.g., VP8, Opus)
- Resolutions and frame rates
- Bit rates
- Encryption parameters (`aes_key`, `aes_iv_mask`)
```cpp
// Sender-side example
openscreen::cast::Offer offer;
offer.cast_mode = openscreen::cast::CastMode::kMirroring;
openscreen::cast::AudioStream audio_stream;
audio_stream.stream.index = 0;
audio_stream.stream.type = openscreen::cast::Stream::Type::kAudioSource;
audio_stream.codec = openscreen::cast::AudioCodec::kOpus;
// ... and other audio parameters ...
offer.audio_streams.push_back(audio_stream);
openscreen::cast::VideoStream video_stream;
video_stream.stream.index = 1;
// ... and other video parameters ...
offer.video_streams.push_back(video_stream);
// The offer is then typically serialized to JSON and sent to the receiver
// over a separate control channel (e.g., a Cast V2 message).
```
2. **The Receiver Responds with an `Answer`**: The receiver application receives
the `Offer`, parses it, and determines if it can accept the proposed
stream(s). It then constructs an `Answer` object, which specifies the
parameters it has agreed to. Key fields in the `Answer` include:
- `udp_port`: The port on which the `Receiver` will listen for RTP packets.
- `send_indexes`: A list of indexes corresponding to the streams in the
`Offer` that the receiver has chosen to accept.
- `ssrcs`: The synchronization source identifiers the receiver will use.
The `Answer` is serialized and sent back to the sender application.
This negotiation happens at the application level, before the `Sender` and
`Receiver` objects are instantiated. The result of this exchange is a
`SessionConfig` object that is used to initialize both sides of the stream.
### Configuration and Instantiation
Once the Offer/Answer exchange is complete, both the sender and receiver have
the necessary `SessionConfig`. They can then create the `Sender` and `Receiver`
objects.
- **Sender Instantiation**:
```cpp
// Sender-side example
#include "cast/streaming/public/sender.h"
#include "cast/streaming/public/environment.h"
// ...
// The SessionConfig is created from the Offer and Answer.
const auto session_config =
SessionConfig::Create(offer, answer);
// An Environment provides platform dependencies like a clock and task runner.
openscreen::cast::Environment environment(...);
openscreen::cast::Sender sender(environment,
packet_router, // Manages network sockets
session_config,
rtp_payload_type);
```
- **Receiver Instantiation**:
```cpp
// Receiver-side example
#include "cast/streaming/public/receiver.h"
#include "cast/streaming/public/environment.h"
// ...
// The SessionConfig is created from the Offer and Answer.
const auto session_config =
SessionConfig::Create(offer, answer);
openscreen::cast::Environment environment(...);
openscreen::cast::Receiver receiver(environment,
packet_router, // Manages network sockets
session_config);
```
### The Client Pattern
Both the `Sender` and `Receiver` are designed to be used with a delegate (or
client) pattern. The application provides an implementation of an interface to
receive asynchronous events and data.
- **`Sender::Observer`**: The application implements the `Sender::Observer`
interface to handle events from the `Sender`. This is crucial for reacting to
network conditions and receiver status.
```cpp
class MySenderClient : public openscreen::cast::Sender::Observer {
public:
void OnPictureLost() override {
// The receiver has indicated it needs a key frame to continue.
// The application should request one from its encoder.
if (sender_->NeedsKeyFrame()) {
RequestNewKeyFrameFromEncoder();
}
}
void OnFrameCanceled(openscreen::cast::FrameId frame_id) override {
// The sender is done with this frame (it was either acknowledged or
// skipped). The application can now free any resources associated
// with it.
}
// ...
};
```
- **`Receiver::Consumer`**: The application implements the `Receiver::Consumer`
interface to be notified when new media frames are ready for consumption.
```cpp
class MyPlayer : public openscreen::cast::Receiver::Consumer {
public:
void OnFramesReady(size_t next_frame_buffer_size) override {
// The Receiver has one or more frames ready.
std::vector<uint8_t> buffer(next_frame_buffer_size);
openscreen::cast::EncodedFrame frame =
receiver_->ConsumeNextFrame(std::move(buffer));
// Pass the frame data to the decoder.
decoder_->Decode(frame.data);
}
// ...
};
```
### Handling Media Frames
- **Sending Frames**: The sender application gets encoded frames from its media
source (e.g., a hardware encoder) and passes them to the `Sender`.
```cpp
// Sender-side example
openscreen::cast::EncodedFrame frame;
frame.frame_id = sender.GetNextFrameId();
frame.dependency = is_key_frame ?
openscreen::cast::EncodedFrame::Dependency::kKeyFrame :
openscreen::cast::EncodedFrame::Dependency::kDependent;
frame.rtp_timestamp = ...;
frame.data = ...; // Pointer to encoded frame data
sender.EnqueueFrame(frame);
```
- **Receiving Frames**: The `Receiver` collects incoming RTP packets and
reassembles them. When a full frame is ready, it notifies the application via
the `OnFramesReady()` callback on the `Receiver::Consumer`. The application
can then call `ConsumeNextFrame()` to retrieve the frame data for decoding and
playback.
### Input Event API
The Input Event API allows a `Receiver` to send input events (like mouse or
keyboard events) back to the `Sender`. This is a unidirectional flow from
receiver to sender.
- [**`cast::InputProducer`**](./input_producer.h): Used by the receiver to send
events. It is available via the `ConfiguredReceivers` struct during session
negotiation.
- [**`cast::InputConsumer`**](./input_consumer.h): Used by the sender to receive
events. Applications set an `InputConsumer::Client` on the `SenderSession`.
Support for input events is negotiated during the Offer/Answer exchange via the
`rtp_extensions` field ("input_events").
## Advanced Topics
### A Note on Packet Routers
The `Sender` and `Receiver` classes do not interact directly with the network.
Instead, they are connected to the network via Packet Routers:
- [`SenderPacketRouter`](./impl/sender_packet_router.h): This class manages
packet transmission for one or more `Sender`s. It paces outbound packets in
bursts to optimize for network conditions (especially WiFi) and can be used to
implement congestion control. `Sender`s are registered with the router and
request transmission slots, rather than sending packets directly.
- [`ReceiverPacketRouter`](./impl/receiver_packet_router.h): This router is
responsible for handling all incoming network traffic from a sender. It
dispatches packets to the appropriate `Receiver` (e.g., for an audio or video
stream) based on the SSRC of the packet.
In a typical application, you will have one packet router on each side of the
connection, managing all the `Sender` or `Receiver` instances for that
connection.
### RTP/RTCP Implementation
The RTP/RTCP implementation in `libcast` is a **custom, from-scratch
implementation** tailored specifically for the Cast protocol. It does not use
external third-party libraries for RTP/RTCP and does not explicitly reference
specific RFCs like RFC 3550. While it follows the spirit of these standards, it
is optimized for Cast use cases.
The core implementation of the RTP and RTCP protocols, including packet parsing,
frame construction, and feedback messaging, is located in the `impl`
sub-directory. The public API abstracts away most of these details, but
developers working on the core streaming logic may need to interact with these
components.
For those looking to understand the implementation, the best places to start are
probably the [`RtpPacketizer`](./impl/rtp_packetizer.h) and the
[`CompoundRtcpBuilder`](./impl/compound_rtcp_builder.h) classes.