Implement Cast Streaming mirroring, DLNA casting, daemon+GUI, and breadd integration
Some checks failed
dev release / build (push) Failing after 12s

Builds out the full v1 scope: a vendored+patched openscreen subset for
low-latency Cast Streaming (Mirroring receiver 0F5096E8) alongside the
existing Cast V2/HLS and new DLNA/AVTransport casting paths, breadcastd's
Idle/Casting state machine with a private IPC socket, the breadcast GTK4
popup as a thin IPC client, and bread.cast.*/bread.command.cast.* breadd
integration (device discovery, start/stop, mirroring lifecycle events).
Also adds bakery/systemd/Forgejo CI packaging.

Validated end-to-end against a real Chromecast/Google TV: negotiated
Cast Streaming session, live pipeline playback, and daemon+GUI click-to-cast/
stop through the actual popup.
This commit is contained in:
Breadway 2026-08-03 09:07:21 +08:00
parent 887c29002f
commit 8c745d18e0
283 changed files with 36788 additions and 0 deletions

View file

@ -0,0 +1,297 @@
#include "facade.h"
#include <atomic>
#include <future>
#include <memory>
#include <string>
#include <vector>
#include "cast/streaming/public/encoded_frame.h"
#include "cast/streaming/public/environment.h"
#include "cast/streaming/rtp_time.h"
#include "message_port_bridge.h"
#include "platform/api/time.h"
#include "platform/base/ip_address.h"
#include "platform/impl/platform_client_posix.h"
#include "platform/impl/task_runner.h"
#include "session.h"
namespace {
// How often to refresh the atomics polled by
// breadcast_caststream_sender_{needs_key_frame,estimated_bandwidth_bps}.
// These are advisory (encoder bitrate/keyframe hints), so a small amount of
// staleness is fine -- this just needs to be fast enough not to be the
// bottleneck in reacting to network conditions.
constexpr std::chrono::milliseconds kPollInterval(100);
// Used as the pre-negotiation default so the very first frames (sent before
// any RTCP feedback exists to compute a real estimate from) aren't wildly
// over-encoded. Conservative middle-of-the-road value; real estimates from
// BandwidthEstimator::ComputeNetworkBandwidth() take over once available.
constexpr int32_t kDefaultBandwidthEstimateBps = 2 * 1000 * 1000;
} // namespace
struct CastStreamSender {
std::unique_ptr<openscreen::cast::Environment> environment;
std::unique_ptr<breadcast_caststream::MessagePortBridge> message_port;
std::unique_ptr<breadcast_caststream::MirroringSenderSession> session;
// Set once, the first time a frame is enqueued after negotiation
// completes; used to compute monotonically-increasing RTP timestamps
// relative to session start.
bool have_origin = false;
int64_t origin_capture_time_us = 0;
// negotiated uses acquire/release so that once
// breadcast_caststream_sender_enqueue_frame observes it true (from an
// arbitrary caller thread), `session->video_sender()` is guaranteed
// already set -- see OnNegotiatedTrampoline below.
std::atomic<bool> negotiated{false};
std::atomic<bool> needs_key_frame{true};
std::atomic<int32_t> estimated_bandwidth_bps{kDefaultBandwidthEstimateBps};
// The caller's own user_data + callbacks, as passed to `_create`. Not
// called directly -- session.h/message_port_bridge.h are instead given
// trampolines below (with `this` as their user_data) so this struct can
// update its own bookkeeping (e.g. `negotiated`) before forwarding.
void* rust_user_data = nullptr;
BreadcastOnNegotiatedFn rust_on_negotiated = nullptr;
BreadcastOnErrorFn rust_on_error = nullptr;
BreadcastOnPictureLostFn rust_on_picture_lost = nullptr;
void SchedulePoll() {
environment->task_runner().PostTaskWithDelay(
[this] {
if (session && session->video_sender()) {
needs_key_frame.store(session->video_sender()->NeedsKeyFrame(),
std::memory_order_relaxed);
const int bps = session->GetEstimatedBandwidthBps();
if (bps > 0) {
estimated_bandwidth_bps.store(bps, std::memory_order_relaxed);
}
}
SchedulePoll();
},
kPollInterval);
}
};
namespace {
void OnNegotiatedTrampoline(void* user_data) {
auto* sender = static_cast<CastStreamSender*>(user_data);
sender->negotiated.store(true, std::memory_order_release);
if (sender->rust_on_negotiated) {
sender->rust_on_negotiated(sender->rust_user_data);
}
}
void OnErrorTrampoline(void* user_data, const char* message, size_t message_len) {
auto* sender = static_cast<CastStreamSender*>(user_data);
if (sender->rust_on_error) {
sender->rust_on_error(sender->rust_user_data, message, message_len);
}
}
void OnPictureLostTrampoline(void* user_data) {
auto* sender = static_cast<CastStreamSender*>(user_data);
if (sender->rust_on_picture_lost) {
sender->rust_on_picture_lost(sender->rust_user_data);
}
}
} // namespace
extern "C" {
CastStreamSender* breadcast_caststream_sender_create(
const char* remote_ip,
size_t remote_ip_len,
const char* local_source_id,
size_t local_source_id_len,
const char* receiver_id,
size_t receiver_id_len,
int32_t width,
int32_t height,
int32_t max_bitrate_bps,
int32_t max_frame_rate_numerator,
int32_t max_frame_rate_denominator,
void* user_data,
BreadcastPostMessageFn post_message,
BreadcastOnNegotiatedFn on_negotiated,
BreadcastOnErrorFn on_error,
BreadcastOnPictureLostFn on_picture_lost) {
auto address_result =
openscreen::IPAddress::Parse(std::string(remote_ip, remote_ip_len));
if (!address_result) {
return nullptr;
}
auto handle = std::make_unique<CastStreamSender>();
handle->rust_user_data = user_data;
handle->rust_on_negotiated = on_negotiated;
handle->rust_on_error = on_error;
handle->rust_on_picture_lost = on_picture_lost;
// Spins up openscreen's TaskRunner + networking threads. Safe to call more
// than once process-wide only if ShutDown() was called first -- breadcast
// only ever has one active cast-streaming session at a time, so this
// assumption (baked into PlatformClientPosix's own singleton design) holds.
openscreen::PlatformClientPosix::Create(std::chrono::milliseconds(50));
openscreen::TaskRunner& task_runner =
openscreen::PlatformClientPosix::GetInstance()->GetTaskRunner();
breadcast_caststream::VideoParams params;
params.width = width;
params.height = height;
params.max_bit_rate = max_bitrate_bps;
params.max_frame_rate_numerator = max_frame_rate_numerator;
params.max_frame_rate_denominator = max_frame_rate_denominator;
breadcast_caststream::SessionCallbacks callbacks;
callbacks.user_data = handle.get();
callbacks.on_negotiated = &OnNegotiatedTrampoline;
callbacks.on_error = &OnErrorTrampoline;
callbacks.on_picture_lost = &OnPictureLostTrampoline;
const openscreen::IPAddress remote_address = address_result.value();
std::string local_source_id_str(local_source_id, local_source_id_len);
std::string receiver_id_str(receiver_id, receiver_id_len);
// Environment's constructor synchronously creates and binds a UdpSocket,
// whose posix implementation asserts it's only ever touched from the
// TaskRunner thread (see udp_socket_posix.cc) -- so construction has to
// happen there too, not on this (arbitrary caller's) thread.
CastStreamSender* handle_ptr = handle.get();
std::promise<void> constructed;
std::future<void> constructed_future = constructed.get_future();
task_runner.PostTask([handle_ptr, &task_runner, &params, &callbacks, remote_address,
local_source_id_str, receiver_id_str, post_message, user_data,
&constructed] {
handle_ptr->environment =
std::make_unique<openscreen::cast::Environment>(&openscreen::Clock::now, task_runner);
handle_ptr->message_port =
std::make_unique<breadcast_caststream::MessagePortBridge>(user_data, post_message);
handle_ptr->session = std::make_unique<breadcast_caststream::MirroringSenderSession>(
*handle_ptr->environment, *handle_ptr->message_port, remote_address,
local_source_id_str, receiver_id_str, params, callbacks);
constructed.set_value();
});
constructed_future.wait();
return handle.release();
}
void breadcast_caststream_sender_negotiate(CastStreamSender* sender) {
sender->environment->task_runner().PostTask([sender] {
sender->session->Negotiate();
sender->SchedulePoll();
});
}
void breadcast_caststream_sender_on_message(CastStreamSender* sender,
const char* source_id,
size_t source_id_len,
const char* message_namespace,
size_t message_namespace_len,
const char* message,
size_t message_len) {
auto source = std::make_shared<std::string>(source_id, source_id_len);
auto ns = std::make_shared<std::string>(message_namespace, message_namespace_len);
auto body = std::make_shared<std::string>(message, message_len);
sender->environment->task_runner().PostTask([sender, source, ns, body] {
sender->message_port->DeliverMessage(*source, *ns, *body);
});
}
int32_t breadcast_caststream_sender_enqueue_frame(CastStreamSender* sender,
const uint8_t* data,
size_t data_len,
int32_t is_key_frame,
int64_t capture_time_us) {
if (!sender->negotiated.load(std::memory_order_acquire)) {
return -1;
}
// Copied here (not captured as a borrowed span) because PostTask defers
// execution -- `data` is only guaranteed valid for the duration of this
// call, per facade.h's documented contract.
auto owned_data = std::make_shared<std::vector<uint8_t>>(data, data + data_len);
const bool is_key = is_key_frame != 0;
sender->environment->task_runner().PostTask([sender, owned_data, is_key, capture_time_us] {
using namespace openscreen;
using namespace openscreen::cast;
Sender* video_sender = sender->session->video_sender();
if (!video_sender) {
return;
}
if (!sender->have_origin) {
sender->have_origin = true;
sender->origin_capture_time_us = capture_time_us;
}
const FrameId frame_id = video_sender->GetNextFrameId();
const FrameId referenced_frame_id =
is_key || frame_id == FrameId::first() ? frame_id : frame_id - 1;
// breadcast's encoder (vah264enc via GStreamer) is assumed to produce a
// simple linear IPPP GOP structure (no B-frames / no multi-reference),
// so "depends on the immediately preceding frame" is a correct
// reference, not just an approximation.
const int64_t elapsed_us = capture_time_us - sender->origin_capture_time_us;
const RtpTimeTicks rtp_timestamp = RtpTimeTicks::FromTimeSinceOrigin(
std::chrono::microseconds(elapsed_us), video_sender->config().rtp_timebase);
const EncodedFrame::Dependency dependency =
is_key ? EncodedFrame::Dependency::kKeyFrame : EncodedFrame::Dependency::kDependent;
EncodedFrame frame(dependency, frame_id, referenced_frame_id, rtp_timestamp, Clock::now(),
/*new_playout_delay=*/std::chrono::milliseconds::zero(),
ByteView(owned_data->data(), owned_data->size()));
// EnqueueFrame()'s result (e.g. MAX_DURATION_IN_FLIGHT under backpressure)
// isn't propagated to the caller: by the time this runs, enqueue_frame()
// has already returned 0 synchronously (this call is posted, not
// immediate -- see facade.h's threading contract). Backpressure here just
// means this one frame is dropped; the encoder finds out indirectly via
// needs_key_frame()/estimated_bandwidth_bps() polling.
(void)video_sender->EnqueueFrame(frame);
});
return 0;
}
int32_t breadcast_caststream_sender_needs_key_frame(CastStreamSender* sender) {
return sender->needs_key_frame.load(std::memory_order_relaxed) ? 1 : 0;
}
int32_t breadcast_caststream_sender_estimated_bandwidth_bps(CastStreamSender* sender) {
return sender->estimated_bandwidth_bps.load(std::memory_order_relaxed);
}
void breadcast_caststream_sender_destroy(CastStreamSender* sender) {
if (!sender) {
return;
}
// These must be torn down on the TaskRunner thread (they hold raw
// references into it and into `environment`), so hop over there and block
// until it's done before shutting the TaskRunner itself down.
std::promise<void> done;
std::future<void> done_future = done.get_future();
sender->environment->task_runner().PostTask([sender, &done] {
sender->session.reset();
sender->message_port.reset();
sender->environment.reset();
done.set_value();
});
done_future.wait();
openscreen::PlatformClientPosix::ShutDown();
delete sender;
}
} // extern "C"

View file

@ -0,0 +1,123 @@
// The extern "C" surface breadcast-caststream-sys's build.rs compiles and
// the Rust side (src/lib.rs) declares `extern "C"` bindings for.
//
// Threading contract: `breadcast_caststream_sender_create` spins up
// openscreen's own TaskRunner + networking threads internally (via
// PlatformClientPosix) -- callers don't manage those. All
// `breadcast_caststream_sender_*` functions taking a `CastStreamSender*` are
// safe to call from any single thread (they internally marshal onto the
// TaskRunner thread via TaskRunner::PostTask, which is documented
// thread-safe) -- but the *callbacks* passed to `_create` fire FROM that
// TaskRunner thread, not the caller's thread. This mirrors the existing
// single-io-thread actor pattern breadcast-core's CastSession already uses
// for CASTV2: the Rust wrapper is expected to run one dedicated thread that
// owns the CastStreamSender and treats callback invocations as arriving from
// a foreign thread (e.g. hands them off over an mpsc channel), exactly like
// CastSession's io loop already does for rust_cast's own callbacks.
#ifndef BREADCAST_CASTSTREAM_FACADE_H_
#define BREADCAST_CASTSTREAM_FACADE_H_
#include <cstddef>
#include <cstdint>
extern "C" {
typedef struct CastStreamSender CastStreamSender;
// Forwards an outbound OFFER/ANSWER-exchange message to Rust for sending
// over the existing CASTV2 urn:x-cast:com.google.cast.webrtc channel. All
// buffers are borrowed for the duration of the call only.
typedef void (*BreadcastPostMessageFn)(void* user_data,
const char* destination_id,
size_t destination_id_len,
const char* message_namespace,
size_t message_namespace_len,
const char* message,
size_t message_len);
// Fired once OFFER/ANSWER negotiation succeeds and the video sender is
// ready to accept frames.
typedef void (*BreadcastOnNegotiatedFn)(void* user_data);
// Fired on a negotiation or session error. `message` is borrowed for the
// duration of the call only.
typedef void (*BreadcastOnErrorFn)(void* user_data,
const char* message,
size_t message_len);
// Fired when the receiver reports picture loss and wants a key frame ASAP
// (a push notification; see also breadcast_caststream_sender_needs_key_frame
// for the pull-style equivalent, which also catches this condition).
typedef void (*BreadcastOnPictureLostFn)(void* user_data);
// Creates a session and starts openscreen's TaskRunner/networking threads.
// `remote_ip` is the receiver's IP address (the same one rust_cast already
// connected to for the CASTV2 control channel); `local_source_id`/
// `receiver_id` are the CASTV2 source/destination IDs to use when sending
// messages over the webrtc namespace (already known to the Rust caller from
// its existing CASTV2 session). Returns null on failure (e.g. invalid IP or
// failure to bind the local UDP socket).
CastStreamSender* breadcast_caststream_sender_create(
const char* remote_ip,
size_t remote_ip_len,
const char* local_source_id,
size_t local_source_id_len,
const char* receiver_id,
size_t receiver_id_len,
int32_t width,
int32_t height,
int32_t max_bitrate_bps,
int32_t max_frame_rate_numerator,
int32_t max_frame_rate_denominator,
void* user_data,
BreadcastPostMessageFn post_message,
BreadcastOnNegotiatedFn on_negotiated,
BreadcastOnErrorFn on_error,
BreadcastOnPictureLostFn on_picture_lost);
// Sends the OFFER and begins waiting for an ANSWER.
void breadcast_caststream_sender_negotiate(CastStreamSender* sender);
// Delivers a message received on the webrtc namespace (e.g. the ANSWER)
// into the session. All buffers are copied before this returns.
void breadcast_caststream_sender_on_message(CastStreamSender* sender,
const char* source_id,
size_t source_id_len,
const char* message_namespace,
size_t message_namespace_len,
const char* message,
size_t message_len);
// Enqueues one encoded video access unit (Annex-B H.264) for sending.
// `data` is copied before this returns, so the caller may reuse/free its
// buffer immediately after. `capture_time_us` is only used to derive the
// RTP timestamp's relative spacing between frames (it does not need to be
// wall-clock-accurate, just monotonically increasing and proportional to
// real elapsed time between frames). Returns 0 if queued, nonzero if the
// session isn't negotiated yet or the frame was rejected (e.g. too large,
// or the in-flight queue is full -- the caller should back off encoding
// when this happens rather than treating it as fatal).
int32_t breadcast_caststream_sender_enqueue_frame(CastStreamSender* sender,
const uint8_t* data,
size_t data_len,
int32_t is_key_frame,
int64_t capture_time_us);
// True (nonzero) if the receiver wants a key frame as soon as possible.
// Safe to poll frequently; cheap, non-blocking, lock-free.
int32_t breadcast_caststream_sender_needs_key_frame(CastStreamSender* sender);
// Best-effort current bandwidth estimate in bits per second. Safe to poll
// frequently; cheap, non-blocking, lock-free. Intended to drive the video
// encoder's target bitrate (this vendored subset of openscreen only does
// flow control, not congestion control -- see Sender's class comment in
// vendor/openscreen/cast/streaming/public/sender.h).
int32_t breadcast_caststream_sender_estimated_bandwidth_bps(CastStreamSender* sender);
// Tears down the session and stops openscreen's internal threads. Blocks
// until shutdown completes.
void breadcast_caststream_sender_destroy(CastStreamSender* sender);
} // extern "C"
#endif // BREADCAST_CASTSTREAM_FACADE_H_

View file

@ -0,0 +1,133 @@
//! Raw FFI bindings to `src/facade.h`/`src/facade.cc`, which wrap a pruned,
//! vendored subset of `chromium/openscreen`'s Cast Streaming sender (see
//! `vendor/openscreen/PATCHES.md`). This crate is intentionally low-level and
//! unsafe -- see `breadcast-caststream` (not this crate) for the ergonomic,
//! thread-safe wrapper most callers should use instead.
//!
//! # Threading contract
//!
//! `sender_create` spins up openscreen's own TaskRunner + networking threads
//! internally; callers don't manage those. Every `sender_*` function taking
//! a `*mut CastStreamSender` is safe to call from any thread (calls are
//! internally marshaled onto the TaskRunner thread). The callbacks passed to
//! `sender_create`, however, fire FROM that TaskRunner thread, not the
//! caller's thread -- see `facade.h`'s doc comment for the full contract,
//! which mirrors the single-io-thread actor pattern breadcast-core's
//! `CastSession` already uses for CASTV2.
use std::ffi::{c_char, c_void};
#[repr(C)]
pub struct CastStreamSender {
_private: [u8; 0],
}
// Safety: every function below is documented (facade.h) as safe to call
// from any thread; only the callbacks fire cross-thread, and those are
// plain `extern "C" fn` pointers rather than captured state, so there is no
// non-Send/Sync data hanging off `*mut CastStreamSender` itself.
unsafe impl Send for CastStreamSender {}
pub type PostMessageFn = extern "C" fn(
user_data: *mut c_void,
destination_id: *const c_char,
destination_id_len: usize,
message_namespace: *const c_char,
message_namespace_len: usize,
message: *const c_char,
message_len: usize,
);
pub type OnNegotiatedFn = extern "C" fn(user_data: *mut c_void);
pub type OnErrorFn =
extern "C" fn(user_data: *mut c_void, message: *const c_char, message_len: usize);
pub type OnPictureLostFn = extern "C" fn(user_data: *mut c_void);
unsafe extern "C" {
/// Returns null on failure (e.g. an unparseable `remote_ip`, or the
/// local UDP socket failed to bind).
///
/// # Safety
/// `remote_ip`/`local_source_id`/`receiver_id` must each point to
/// `_len` valid, readable bytes for the duration of this call.
/// `post_message`/`on_negotiated`/`on_error`/`on_picture_lost` must be
/// valid to call for as long as the returned sender is alive (i.e.
/// until `sender_destroy` returns). `user_data` is passed back
/// unmodified to every callback and may be null.
pub fn breadcast_caststream_sender_create(
remote_ip: *const c_char,
remote_ip_len: usize,
local_source_id: *const c_char,
local_source_id_len: usize,
receiver_id: *const c_char,
receiver_id_len: usize,
width: i32,
height: i32,
max_bitrate_bps: i32,
max_frame_rate_numerator: i32,
max_frame_rate_denominator: i32,
user_data: *mut c_void,
post_message: PostMessageFn,
on_negotiated: OnNegotiatedFn,
on_error: OnErrorFn,
on_picture_lost: OnPictureLostFn,
) -> *mut CastStreamSender;
/// # Safety
/// `sender` must be a live pointer returned by `sender_create` and not
/// yet passed to `sender_destroy`.
pub fn breadcast_caststream_sender_negotiate(sender: *mut CastStreamSender);
/// Delivers an inbound message (e.g. the receiver's ANSWER) received on
/// the CASTV2 `urn:x-cast:com.google.cast.webrtc` namespace into the
/// session. All buffers are copied before this returns.
///
/// # Safety
/// `sender` must be live. `source_id`/`message_namespace`/`message` must
/// each point to `_len` valid, readable bytes for the duration of this
/// call only.
pub fn breadcast_caststream_sender_on_message(
sender: *mut CastStreamSender,
source_id: *const c_char,
source_id_len: usize,
message_namespace: *const c_char,
message_namespace_len: usize,
message: *const c_char,
message_len: usize,
);
/// Enqueues one encoded video access unit (Annex-B H.264) for sending.
/// Returns 0 if queued, nonzero if not negotiated yet.
///
/// # Safety
/// `sender` must be live. `data` must point to `data_len` valid,
/// readable bytes for the duration of this call only (it is copied
/// before this returns).
pub fn breadcast_caststream_sender_enqueue_frame(
sender: *mut CastStreamSender,
data: *const u8,
data_len: usize,
is_key_frame: i32,
capture_time_us: i64,
) -> i32;
/// # Safety
/// `sender` must be live.
pub fn breadcast_caststream_sender_needs_key_frame(sender: *mut CastStreamSender) -> i32;
/// # Safety
/// `sender` must be live.
pub fn breadcast_caststream_sender_estimated_bandwidth_bps(
sender: *mut CastStreamSender,
) -> i32;
/// Tears down the session and blocks until openscreen's internal
/// threads stop. `sender` must not be used again after this call.
///
/// # Safety
/// `sender` must be a live pointer returned by `sender_create`, not
/// already passed to this function.
pub fn breadcast_caststream_sender_destroy(sender: *mut CastStreamSender);
}

View file

@ -0,0 +1,35 @@
#include "message_port_bridge.h"
namespace breadcast_caststream {
MessagePortBridge::MessagePortBridge(void* user_data,
PostMessageCallback post_message)
: user_data_(user_data), post_message_(post_message) {}
MessagePortBridge::~MessagePortBridge() = default;
void MessagePortBridge::DeliverMessage(const std::string& source_id,
const std::string& message_namespace,
const std::string& message) {
if (client_) {
client_->OnMessage(source_id, message_namespace, message);
}
}
void MessagePortBridge::SetClient(Client& client) {
client_ = &client;
}
void MessagePortBridge::ResetClient() {
client_ = nullptr;
}
void MessagePortBridge::PostMessage(const std::string& destination_id,
const std::string& message_namespace,
const std::string& message) {
post_message_(user_data_, destination_id.data(), destination_id.size(),
message_namespace.data(), message_namespace.size(),
message.data(), message.size());
}
} // namespace breadcast_caststream

View file

@ -0,0 +1,58 @@
// A openscreen::cast::MessagePort implementation that forwards outbound
// messages to a Rust-provided callback (which sends them over the existing
// rust_cast-managed CASTV2 TLS channel, in the
// urn:x-cast:com.google.cast.webrtc namespace) and accepts inbound messages
// via DeliverMessage(), called by Rust when a reply arrives on that
// channel. This is what lets openscreen's own SenderSessionMessenger/
// SenderPacketRouter run OFFER/ANSWER negotiation without this crate
// needing its own TLS stack -- see ../vendor/openscreen/PATCHES.md.
#ifndef BREADCAST_CASTSTREAM_MESSAGE_PORT_BRIDGE_H_
#define BREADCAST_CASTSTREAM_MESSAGE_PORT_BRIDGE_H_
#include <cstddef>
#include <cstdint>
#include <string>
#include "cast/common/public/message_port.h"
namespace breadcast_caststream {
// Matches the PostMessageFn typedef in facade.h.
using PostMessageCallback = void (*)(void* user_data,
const char* destination_id,
size_t destination_id_len,
const char* message_namespace,
size_t message_namespace_len,
const char* message,
size_t message_len);
class MessagePortBridge final : public openscreen::cast::MessagePort {
public:
MessagePortBridge(void* user_data, PostMessageCallback post_message);
~MessagePortBridge() override;
// Called by facade.cc's FFI entry point when Rust has received a message
// on the webrtc namespace for us. Safe to call from any thread; the
// caller is responsible for making sure this only actually touches
// `client_` while running on the Environment's TaskRunner thread (facade.cc
// marshals this via TaskRunner::PostTask before calling here).
void DeliverMessage(const std::string& source_id,
const std::string& message_namespace,
const std::string& message);
// openscreen::cast::MessagePort implementation.
void SetClient(Client& client) override;
void ResetClient() override;
void PostMessage(const std::string& destination_id,
const std::string& message_namespace,
const std::string& message) override;
private:
void* const user_data_;
const PostMessageCallback post_message_;
Client* client_ = nullptr;
};
} // namespace breadcast_caststream
#endif // BREADCAST_CASTSTREAM_MESSAGE_PORT_BRIDGE_H_

View file

@ -0,0 +1,181 @@
#include "session.h"
#include <utility>
#include "cast/streaming/impl/rtp_defines.h"
#include "cast/streaming/message_fields.h"
#include "cast/streaming/public/constants.h"
#include "cast/streaming/public/session_config.h"
#include "cast/streaming/sender_message.h"
#include "util/crypto/random_bytes.h"
namespace breadcast_caststream {
namespace {
using openscreen::Error;
using openscreen::ErrorOr;
using openscreen::GenerateRandomBytes16;
using openscreen::cast::AudioStream;
using openscreen::cast::CastMode;
using openscreen::cast::GenerateSsrc;
using openscreen::cast::GetPayloadType;
using openscreen::cast::kMinVideoHeight;
using openscreen::cast::kMinVideoWidth;
using openscreen::cast::kRtpVideoTimebase;
using openscreen::cast::Offer;
using openscreen::cast::ReceiverMessage;
using openscreen::cast::Resolution;
using openscreen::cast::SenderMessage;
using openscreen::cast::SessionConfig;
using openscreen::cast::Stream;
using openscreen::cast::ToStreamType;
using openscreen::cast::VideoCodec;
using openscreen::cast::VideoStream;
// breadcast always mirrors exactly one video stream at index 0 -- there is
// no audio stream in this integration (breadcast's capture pipeline is
// video-only), so the index scheme sender_session.cc uses for interleaving
// audio-then-video streams collapses to just "index 0 is the video stream."
constexpr int kVideoStreamIndex = 0;
VideoStream BuildVideoStream(const VideoParams& params,
bool use_android_rtp_hack) {
Stream stream;
stream.index = kVideoStreamIndex;
stream.type = Stream::Type::kVideoSource;
stream.channels = 1;
stream.rtp_payload_type = GetPayloadType(VideoCodec::kH264, use_android_rtp_hack);
stream.ssrc = GenerateSsrc(/*higher_priority=*/false);
stream.target_delay = openscreen::cast::kDefaultTargetPlayoutDelay;
stream.aes_key = GenerateRandomBytes16();
stream.aes_iv_mask = GenerateRandomBytes16();
stream.receiver_rtcp_event_log = true;
stream.rtp_timebase = kRtpVideoTimebase;
VideoStream video_stream;
video_stream.stream = std::move(stream);
video_stream.codec = VideoCodec::kH264;
video_stream.max_frame_rate = openscreen::SimpleFraction{
params.max_frame_rate_numerator, params.max_frame_rate_denominator};
video_stream.max_bit_rate = (params.max_bit_rate >= openscreen::cast::kDefaultVideoMinBitRate)
? params.max_bit_rate
: openscreen::cast::kDefaultVideoMaxBitRate;
video_stream.resolutions.push_back(Resolution{
std::max(params.width, kMinVideoWidth), std::max(params.height, kMinVideoHeight)});
return video_stream;
}
} // namespace
MirroringSenderSession::MirroringSenderSession(
openscreen::cast::Environment& environment,
openscreen::cast::MessagePort& message_port,
openscreen::IPAddress remote_address,
std::string local_source_id,
std::string receiver_id,
VideoParams params,
SessionCallbacks callbacks)
: environment_(environment),
remote_address_(remote_address),
receiver_id_(std::move(receiver_id)),
params_(params),
callbacks_(callbacks),
messenger_(
message_port,
std::move(local_source_id),
receiver_id_,
[this](Error error) { ReportError(error.message()); },
environment.task_runner()),
packet_router_(environment) {}
MirroringSenderSession::~MirroringSenderSession() {
if (video_sender_) {
video_sender_->SetObserver(nullptr);
}
}
void MirroringSenderSession::Negotiate() {
// use_android_rtp_hack defaults on upstream too (crbug.com/631828) --
// Google TV devices are Android TV under the hood, so this is left on.
constexpr bool kUseAndroidRtpHack = true;
Offer offer;
offer.cast_mode = CastMode::kMirroring;
offer.video_streams.push_back(BuildVideoStream(params_, kUseAndroidRtpHack));
pending_offer_ = offer;
const Error result = messenger_.SendRequest(
SenderMessage{SenderMessage::Type::kOffer, ++sequence_number_,
/*valid=*/true, std::move(offer)},
ReceiverMessage::Type::kAnswer,
[this](ErrorOr<ReceiverMessage> message) { OnAnswer(std::move(message)); });
if (!result.ok()) {
ReportError(result.message());
}
}
void MirroringSenderSession::OnAnswer(ErrorOr<ReceiverMessage> message) {
if (!message) {
ReportError(message.error().message());
return;
}
if (!message.value().valid || message.value().type != ReceiverMessage::Type::kAnswer) {
ReportError("Receiver sent an invalid or unexpected ANSWER response");
return;
}
const auto& answer = std::get<openscreen::cast::Answer>(message.value().body);
if (answer.send_indexes.empty() || answer.ssrcs.empty()) {
ReportError("ANSWER selected no streams");
return;
}
environment_.set_remote_endpoint(
openscreen::IPEndpoint{remote_address_, static_cast<uint16_t>(answer.udp_port)});
const openscreen::cast::VideoStream& stream = pending_offer_.video_streams[0];
const openscreen::cast::RtpPayloadType payload_type = stream.stream.rtp_payload_type;
SessionConfig config{stream.stream.ssrc,
answer.ssrcs[0],
stream.stream.rtp_timebase,
stream.stream.channels,
stream.stream.target_delay,
stream.stream.aes_key,
stream.stream.aes_iv_mask,
/*is_pli_enabled=*/true,
ToStreamType(payload_type, /*use_android_rtp_hack=*/true)};
if (!config.IsValid()) {
ReportError("Derived an invalid SessionConfig from the ANSWER");
return;
}
video_sender_ = std::make_unique<openscreen::cast::SenderImpl>(
environment_, packet_router_, std::move(config), payload_type);
video_sender_->SetObserver(this);
if (callbacks_.on_negotiated) {
callbacks_.on_negotiated(callbacks_.user_data);
}
}
int MirroringSenderSession::GetEstimatedBandwidthBps() const {
return packet_router_.ComputeNetworkBandwidth();
}
void MirroringSenderSession::OnFrameCanceled(openscreen::cast::FrameId) {}
void MirroringSenderSession::OnPictureLost() {
if (callbacks_.on_picture_lost) {
callbacks_.on_picture_lost(callbacks_.user_data);
}
}
void MirroringSenderSession::ReportError(const std::string& message) {
if (callbacks_.on_error) {
callbacks_.on_error(callbacks_.user_data, message.data(), message.size());
}
}
} // namespace breadcast_caststream

View file

@ -0,0 +1,102 @@
// A minimal, video-only Cast Streaming sender negotiation driver.
//
// This deliberately does NOT use openscreen's own
// cast::SenderSession -- that class bundles mirroring negotiation together
// with RPC/remoting/input support, which pulls in protobuf
// (input.pb.h/remoting.pb.h) for no benefit here (breadcast only ever does
// one-way video mirroring). Instead, this reimplements just the
// OFFER-building and ANSWER-handling logic SenderSession itself uses
// internally (see CreateMirroringOffer/StartNegotiation/SelectSenders in
// upstream's public/sender_session.cc), built directly on
// SenderSessionMessenger + Offer/Answer/SessionConfig + SenderImpl. See
// ../vendor/openscreen/PATCHES.md.
#ifndef BREADCAST_CASTSTREAM_SESSION_H_
#define BREADCAST_CASTSTREAM_SESSION_H_
#include <cstddef>
#include <memory>
#include <string>
#include "cast/streaming/impl/sender_impl.h"
#include "cast/streaming/public/environment.h"
#include "cast/streaming/public/offer_messages.h"
#include "cast/streaming/public/receiver_message.h"
#include "cast/streaming/public/sender.h"
#include "cast/streaming/public/session_messenger.h"
#include "cast/streaming/sender_packet_router.h"
#include "platform/base/ip_address.h"
namespace breadcast_caststream {
struct VideoParams {
int width = 1920;
int height = 1080;
int max_bit_rate = 8 * 1000 * 1000;
int max_frame_rate_numerator = 30;
int max_frame_rate_denominator = 1;
};
struct SessionCallbacks {
void* user_data = nullptr;
void (*on_negotiated)(void* user_data) = nullptr;
void (*on_error)(void* user_data, const char* message, size_t message_len) =
nullptr;
void (*on_picture_lost)(void* user_data) = nullptr;
};
// Owns the OFFER/ANSWER exchange and, once negotiated, the resulting video
// Sender. All methods (other than the constructor) must be called on
// `environment`'s TaskRunner thread -- facade.cc is responsible for
// marshaling calls onto it via TaskRunner::PostTask, matching the threading
// contract the rest of openscreen's Environment/SenderPacketRouter/Sender
// classes already assume.
class MirroringSenderSession final : public openscreen::cast::Sender::Observer {
public:
MirroringSenderSession(openscreen::cast::Environment& environment,
openscreen::cast::MessagePort& message_port,
openscreen::IPAddress remote_address,
std::string local_source_id,
std::string receiver_id,
VideoParams params,
SessionCallbacks callbacks);
~MirroringSenderSession() override;
MirroringSenderSession(const MirroringSenderSession&) = delete;
MirroringSenderSession& operator=(const MirroringSenderSession&) = delete;
// Sends the OFFER and begins waiting for an ANSWER. `callbacks.on_negotiated`
// or `callbacks.on_error` will be called once the exchange completes.
void Negotiate();
// Valid only after `on_negotiated` has fired.
openscreen::cast::Sender* video_sender() { return video_sender_.get(); }
// Best-effort current bandwidth estimate in bits per second, or a
// conservative default before enough RTCP feedback has arrived.
int GetEstimatedBandwidthBps() const;
// Sender::Observer implementation.
void OnFrameCanceled(openscreen::cast::FrameId frame_id) override;
void OnPictureLost() override;
private:
void OnAnswer(openscreen::ErrorOr<openscreen::cast::ReceiverMessage> message);
void ReportError(const std::string& message);
openscreen::cast::Environment& environment_;
const openscreen::IPAddress remote_address_;
const std::string receiver_id_;
const VideoParams params_;
const SessionCallbacks callbacks_;
openscreen::cast::SenderSessionMessenger messenger_;
openscreen::cast::SenderPacketRouter packet_router_;
int sequence_number_ = 0;
openscreen::cast::Offer pending_offer_;
std::unique_ptr<openscreen::cast::SenderImpl> video_sender_;
};
} // namespace breadcast_caststream
#endif // BREADCAST_CASTSTREAM_SESSION_H_