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:
parent
9c6fc61f53
commit
5b49955e33
4 changed files with 299 additions and 44 deletions
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue