Fix Cast Streaming teardown crash, bitrate collapse, and HLS latency

Three independent problems found by reading the mirroring paths end to end.

1. Segfault on Cast session teardown. CastStreamSender::SchedulePoll
   self-reschedules every 100ms with a raw `this` and was never cancelled,
   so the one task that can be scheduled to run *after* an already-queued
   teardown task would dereference the just-reset `environment` unique_ptr
   (Environment::task_runner() dereferences a member immediately) --- a hard
   null deref on openscreen's TaskRunner thread. TaskRunnerImpl's shutdown
   has an explicit flushing phase and PlatformClientPosix::ShutDown()'s quit
   task queues behind whatever is already pending, so this is a race the
   teardown path can lose. Latch a `shutting_down` atomic before posting
   teardown and check it in the poll and in every other posted task.

2. Encoder bitrate collapsing to the floor within seconds. The control loop
   set `target = 0.85 * estimate` once a second unconditionally. openscreen's
   BandwidthEstimator deliberately under-estimates capacity whenever the
   transmit rate is below it and documents the required TCP-like response;
   multiplying the target by <=0.85 every second instead walks 4000 kbps past
   1500 in ~6s and pins it at the floor on a healthy LAN. Replaced with
   proper AIMD (hold on a zero/unknown estimate, back off below it, probe up
   10%/s otherwise), clamped to 1000..8000 kbps, with unit tests.

3. DLNA/HLS latency. Segment length is max(target-duration, GOP), so
   target-duration=2 with a 2s GOP gave 2s segments, ~6s of renderer buffer,
   plus 3 segments of stale video waited for before handover. 1s segments
   (GOP halved to make that reachable), shorter playlist, and wait for 2
   segments instead of 3.

Also hardened two paths into openscreen's fatal OSP_CHECK on strictly
increasing RTP timestamps: pull_encoded_frame no longer substitutes 0 for a
missing PTS (it skips the buffer), and facade.cc drops non-monotonic capture
times at the FFI boundary. Either could previously abort the daemon outright.

CastMirrorSession now owns the Arc<CastStreamSender> instead of leaving its
lifetime to whichever detached pump thread dropped the last clone, so the
blocking FFI destroy happens at a defined point in stop() with the pump
joins ordered around it.
This commit is contained in:
Breadway 2026-08-05 19:04:03 +08:00
parent 9c6fc61f53
commit 5b49955e33
4 changed files with 299 additions and 44 deletions

View file

@ -44,6 +44,34 @@ struct CastStreamSender {
bool have_origin = false;
int64_t origin_capture_time_us = 0;
// The capture timestamp of the last frame actually handed to
// Sender::EnqueueFrame. openscreen enforces strictly-increasing RTP
// timestamps with a *fatal* OSP_CHECK_GT (sender_impl.cc), not an error
// return -- so a single frame arriving with a non-increasing capture time
// (a buffer with no PTS, which the GStreamer side substitutes 0 for; a
// clock reset on portal source change; any encoder that ever reorders
// output) would abort the whole process. Dropping such a frame instead
// costs at most one frame of video. Only touched on the TaskRunner
// thread.
bool have_last_capture_time = false;
int64_t last_capture_time_us = 0;
// Set by breadcast_caststream_sender_destroy *before* it posts its
// teardown task, and checked by the self-rescheduling poll below.
//
// Without this, the poll task (posted with a 100ms delay, so it is the one
// task that can be scheduled to run *after* an already-queued teardown
// task) dereferences `environment` after teardown has reset it -- a null
// `unique_ptr<Environment>`, whose `task_runner()` accessor immediately
// dereferences a member -- i.e. a hard SIGSEGV on openscreen's TaskRunner
// thread. If it lands even later it is a use-after-free of `this`, since
// destroy() `delete`s this struct once the teardown task completes.
// TaskRunnerImpl's shutdown has an explicit "flushing phase" that keeps
// running runnable tasks, and PlatformClientPosix::ShutDown()'s quit task
// is queued *behind* whatever is already pending, so this is a race the
// teardown path can and does lose.
std::atomic<bool> shutting_down{false};
// 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
@ -62,8 +90,14 @@ struct CastStreamSender {
BreadcastOnPictureLostFn rust_on_picture_lost = nullptr;
void SchedulePoll() {
if (shutting_down.load(std::memory_order_acquire) || !environment) {
return;
}
environment->task_runner().PostTaskWithDelay(
[this] {
if (shutting_down.load(std::memory_order_acquire)) {
return;
}
if (session && session->video_sender()) {
needs_key_frame.store(session->video_sender()->NeedsKeyFrame(),
std::memory_order_relaxed);
@ -186,6 +220,10 @@ CastStreamSender* breadcast_caststream_sender_create(
void breadcast_caststream_sender_negotiate(CastStreamSender* sender) {
sender->environment->task_runner().PostTask([sender] {
if (sender->shutting_down.load(std::memory_order_acquire) ||
!sender->session) {
return;
}
sender->session->Negotiate();
sender->SchedulePoll();
});
@ -202,6 +240,10 @@ void breadcast_caststream_sender_on_message(CastStreamSender* sender,
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] {
if (sender->shutting_down.load(std::memory_order_acquire) ||
!sender->message_port) {
return;
}
sender->message_port->DeliverMessage(*source, *ns, *body);
});
}
@ -225,15 +267,30 @@ int32_t breadcast_caststream_sender_enqueue_frame(CastStreamSender* sender,
using namespace openscreen;
using namespace openscreen::cast;
if (sender->shutting_down.load(std::memory_order_acquire) ||
!sender->session) {
return;
}
Sender* video_sender = sender->session->video_sender();
if (!video_sender) {
return;
}
// See `last_capture_time_us`: openscreen aborts the process (fatal
// OSP_CHECK, not an error return) if RTP timestamps ever fail to
// strictly increase, so a non-monotonic capture time has to be dropped
// here rather than passed through.
if (sender->have_last_capture_time &&
capture_time_us <= sender->last_capture_time_us) {
return;
}
if (!sender->have_origin) {
sender->have_origin = true;
sender->origin_capture_time_us = capture_time_us;
}
sender->have_last_capture_time = true;
sender->last_capture_time_us = capture_time_us;
const FrameId frame_id = video_sender->GetNextFrameId();
const FrameId referenced_frame_id =
@ -277,6 +334,12 @@ void breadcast_caststream_sender_destroy(CastStreamSender* sender) {
if (!sender) {
return;
}
// Latched *before* the teardown task is posted so the self-rescheduling
// poll (see CastStreamSender::shutting_down) stops re-arming itself and
// no longer touches `environment`/`session` -- both of which the teardown
// task below is about to reset out from under it.
sender->shutting_down.store(true, std::memory_order_release);
// 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.