Implement Cast Streaming mirroring, DLNA casting, daemon+GUI, and breadd integration
Some checks failed
dev release / build (push) Failing after 12s
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:
parent
887c29002f
commit
8c745d18e0
283 changed files with 36788 additions and 0 deletions
297
breadcast-caststream-sys/src/facade.cc
Normal file
297
breadcast-caststream-sys/src/facade.cc
Normal 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, ¶ms, &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"
|
||||
123
breadcast-caststream-sys/src/facade.h
Normal file
123
breadcast-caststream-sys/src/facade.h
Normal 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_
|
||||
133
breadcast-caststream-sys/src/lib.rs
Normal file
133
breadcast-caststream-sys/src/lib.rs
Normal 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);
|
||||
}
|
||||
35
breadcast-caststream-sys/src/message_port_bridge.cc
Normal file
35
breadcast-caststream-sys/src/message_port_bridge.cc
Normal 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
|
||||
58
breadcast-caststream-sys/src/message_port_bridge.h
Normal file
58
breadcast-caststream-sys/src/message_port_bridge.h
Normal 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_
|
||||
181
breadcast-caststream-sys/src/session.cc
Normal file
181
breadcast-caststream-sys/src/session.cc
Normal 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
|
||||
102
breadcast-caststream-sys/src/session.h
Normal file
102
breadcast-caststream-sys/src/session.h
Normal 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_
|
||||
Loading…
Add table
Add a link
Reference in a new issue