Fix Cast teardown leaks, keyframe latch, and DLNA session lifecycle

Dropped frames never requested a keyframe, SessionEnded skipped ordered
stop (portal/TV/FFI leak, next start could abort), and a late end could
kill the following cast. Failed starts left PlatformClientPosix alive.
DLNA leaked its HTTP server and ignored portal EOS.

Also: start no longer blocks the daemon actor, IPC accept/request loops
stay up, HLS Range is clamped, LAN IP follows the renderer subnet, and
the picker closes before the portal dialog and handles Escape.
This commit is contained in:
Breadway 2026-08-16 14:15:56 +08:00
parent a80c49593d
commit 17abeed7ae
26 changed files with 861 additions and 222 deletions

View file

@ -209,9 +209,20 @@ CastStreamSender* breadcast_caststream_sender_create(
// 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.
//
// Create() itself OSP_CHECKs that no instance exists and aborts the
// process if a previous sender leaked (a failed start that never reached
// destroy()). Fail the new create instead of taking the daemon down.
if (openscreen::PlatformClientPosix::GetInstance() != nullptr) {
return nullptr;
}
openscreen::PlatformClientPosix::Create(std::chrono::milliseconds(50));
openscreen::TaskRunner& task_runner =
openscreen::PlatformClientPosix::GetInstance()->GetTaskRunner();
openscreen::PlatformClientPosix* instance =
openscreen::PlatformClientPosix::GetInstance();
if (instance == nullptr) {
return nullptr;
}
openscreen::TaskRunner& task_runner = instance->GetTaskRunner();
breadcast_caststream::VideoParams params;
params.width = width;
@ -255,6 +266,13 @@ CastStreamSender* breadcast_caststream_sender_create(
}
void breadcast_caststream_sender_negotiate(CastStreamSender* sender) {
// environment is reset during destroy() on the TaskRunner thread; a call
// that races teardown (or arrives after a failed start) must not
// dereference the unique_ptr.
if (!sender || sender->shutting_down.load(std::memory_order_acquire) ||
!sender->environment) {
return;
}
sender->environment->task_runner().PostTask([sender] {
if (sender->shutting_down.load(std::memory_order_acquire) ||
!sender->session) {
@ -272,6 +290,10 @@ void breadcast_caststream_sender_on_message(CastStreamSender* sender,
size_t message_namespace_len,
const char* message,
size_t message_len) {
if (!sender || sender->shutting_down.load(std::memory_order_acquire) ||
!sender->environment) {
return;
}
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);
@ -289,6 +311,10 @@ int32_t breadcast_caststream_sender_enqueue_frame(CastStreamSender* sender,
size_t data_len,
int32_t is_key_frame,
int64_t capture_time_us) {
if (!sender || sender->shutting_down.load(std::memory_order_acquire) ||
!sender->environment) {
return -1;
}
if (!sender->negotiated.load(std::memory_order_acquire)) {
return -1;
}
@ -378,6 +404,12 @@ int32_t breadcast_caststream_sender_enqueue_frame(CastStreamSender* sender,
switch (video_sender->EnqueueFrame(frame)) {
case Sender::OK:
sender->enqueue_ok.fetch_add(1, std::memory_order_relaxed);
// A key frame that actually landed resyncs the decoder, so the
// "next frame must be an IDR" latch can clear. A rejected key
// frame leaves the flag set (the reject branches below).
if (is_key) {
sender->frame_chain_broken.store(false, std::memory_order_relaxed);
}
break;
case Sender::PAYLOAD_TOO_LARGE:
sender->enqueue_payload_too_large.fetch_add(1, std::memory_order_relaxed);
@ -418,7 +450,18 @@ void breadcast_caststream_sender_take_stats(CastStreamSender* sender,
}
int32_t breadcast_caststream_sender_needs_key_frame(CastStreamSender* sender) {
return sender->needs_key_frame.load(std::memory_order_relaxed) ? 1 : 0;
if (!sender) {
return 0;
}
// `frame_chain_broken` is the drop-side latch SchedulePoll must not
// clobber (see the field comment). Load, don't exchange: the encoder
// may take several frames to honour a force-key-unit, and a consuming
// read here would forget the drop if the next poll happened before
// that IDR was actually EnqueueFrame'd (cleared on OK + is_key above).
return (sender->frame_chain_broken.load(std::memory_order_relaxed) ||
sender->needs_key_frame.load(std::memory_order_relaxed))
? 1
: 0;
}
int32_t breadcast_caststream_sender_estimated_bandwidth_bps(CastStreamSender* sender) {
@ -437,18 +480,24 @@ void breadcast_caststream_sender_destroy(CastStreamSender* sender) {
// 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();
// until it's done before shutting the TaskRunner itself down. A sender
// whose Environment never got built (create failed mid-flight) has no
// task runner to hop to.
if (sender->environment) {
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();
if (openscreen::PlatformClientPosix::GetInstance() != nullptr) {
openscreen::PlatformClientPosix::ShutDown();
}
delete sender;
}

View file

@ -88,15 +88,18 @@ void breadcast_caststream_sender_on_message(CastStreamSender* sender,
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).
// Posts one encoded video access unit (Annex-B H.264) onto openscreen's
// TaskRunner 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 the frame was *posted* (not necessarily accepted --
// Sender::EnqueueFrame runs later on the TaskRunner and may still reject
// it), or nonzero if the session isn't negotiated yet / is shutting down.
// Accept/reject outcomes are visible only via take_stats() and, for
// dropped frames that break the H.264 reference chain, needs_key_frame().
int32_t breadcast_caststream_sender_enqueue_frame(CastStreamSender* sender,
const uint8_t* data,
size_t data_len,

View file

@ -1,8 +1,8 @@
//! 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.
//! unsafe -- see `breadcast-core::caststream` for the ergonomic, thread-safe
//! wrapper most callers should use instead.
//!
//! # Threading contract
//!