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,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