From e086f31797797825945b239857c145e899adcd5f Mon Sep 17 00:00:00 2001 From: tofublock Date: Sun, 26 Apr 2026 12:44:59 +0200 Subject: [PATCH] Fix Bambu camera stream freeze on Linux (#9280) (#13359) The X1C's RTSPS server emits unreliable decode timestamps (wildly non-monotonic jumps, sometimes none at all). The previous AVC1 path in gstbambusrc forwarded sample.decode_time straight into the pipeline as DTS, which made GStreamer's basesink wait for the pipeline clock to catch up with timestamps that were thousands of seconds in the future, freezing playback after a few seconds of streaming. Replace the trust-the-printer approach with synthesized monotonic timestamps, the same trick mpv uses when it reports "No video PTS! Making something up.": * stamp PTS = sttime + frame_count * period * adapt period to the real inter-arrival rate via EWMA (the announced frame_rate is also unreliable -- X1C reports 30 fps but delivers ~28 fps), keeping the synthesized timeline aligned with arrival * apply a 100 ms lead so the sink has a small jitter buffer to absorb the bursty 2-3-frames-then-gap delivery pattern of the Bambu tunnel * re-anchor only on large divergence (printer pause, stream resume, fps change) instead of every few seconds The MJPEG path was already wall-clock-stamping arrivals; this unifies both formats on the same scheme. Confirmed on an X1C in LAN-only mode (RTSPS) on Fedora 43; not tested on P1/A1 hardware. Other Bambu printers share the same Linux source and could see the same fix where their decode timestamps or announced framerate are similarly unreliable, but that has not been verified. No effect on Windows/macOS -- gstbambusrc.c is Linux-only. Trade-off is +100 ms of camera-view latency, which is invisible for a print- monitor use case. Co-authored-by: Claude Opus 4.7 (1M context) Co-authored-by: SoftFever --- src/slic3r/GUI/Printer/gstbambusrc.c | 106 ++++++++++++++++++++------- src/slic3r/GUI/Printer/gstbambusrc.h | 4 + 2 files changed, 85 insertions(+), 25 deletions(-) diff --git a/src/slic3r/GUI/Printer/gstbambusrc.c b/src/slic3r/GUI/Printer/gstbambusrc.c index 316432fd0a..d3f4ab112a 100644 --- a/src/slic3r/GUI/Printer/gstbambusrc.c +++ b/src/slic3r/GUI/Printer/gstbambusrc.c @@ -299,34 +299,86 @@ gst_bambusrc_create (GstPushSrc * psrc, GstBuffer ** outbuf) #endif *outbuf = gst_buffer_new_wrapped_full(0, sbuf, sample.size, 0, sample.size, sbuf, g_free); - /* The NAL data already contains a timestamp (I think?), but we seem to - * need to feed this in too -- otherwise the GStreamer pipeline gets upset - * and starts triggering QoS events. + /* Synthesize monotonic timestamps at the announced frame rate, anchored + * to the first frame's arrival time. The X1C's RTSPS server emits + * unreliable decode timestamps (wildly non-monotonic jumps, or sometimes + * none at all); forwarding them directly froze the pipeline after a few + * seconds. Pacing on a synthesized clock — the same trick mpv uses when + * it reports "No video PTS! Making something up." — gives smooth + * playback regardless of network jitter, and only drops late frames if + * the printer can't keep up. A snap-back resets the anchor if real + * arrival drifts more than two frame periods from the synthesized + * timeline (e.g. announced framerate was wrong). */ - if (src->video_type == AVC1) { - if (!src->sttime) { - src->sttime = sample.decode_time * 100ULL; - } - GST_BUFFER_DTS(*outbuf) = sample.decode_time * 100ULL - src->sttime; - GST_BUFFER_PTS(*outbuf) = GST_CLOCK_TIME_NONE; - GST_BUFFER_DURATION(*outbuf) = GST_CLOCK_TIME_NONE; + GstClock *clock = GST_ELEMENT_CLOCK(psrc); + GstClockTime base_time = gst_element_get_base_time((GstElement *)psrc); + GstClockTime running_now = GST_CLOCK_TIME_NONE; + if (clock) { + GstClockTime now = gst_clock_get_time(clock); + if (now != GST_CLOCK_TIME_NONE && now >= base_time) + running_now = now - base_time; } - else { - if (!src->sttime) { - //only available from 1.18 - //src->sttime = gst_element_get_current_clock_time((GstElement *)psrc); - src->sttime = gst_clock_get_time(((GstElement *)psrc)->clock); - //if (GST_CLOCK_TIME_NONE == src->sttime) - // src->sttime - GST_DEBUG_OBJECT(src, - "sttime init to %lu.", - src->sttime); - } - //GST_BUFFER_DTS(*outbuf) = gst_element_get_current_clock_time((GstElement *)psrc) - src->sttime; - GST_BUFFER_DTS(*outbuf) = gst_clock_get_time(((GstElement *)psrc)->clock) - src->sttime; - GST_BUFFER_PTS(*outbuf) = GST_CLOCK_TIME_NONE; - GST_BUFFER_DURATION(*outbuf) = GST_CLOCK_TIME_NONE; + + /* Adapt the period to actual inter-arrival time via EWMA. The announced + * frame_rate is unreliable on Bambu printers (X1C announces 30 but + * delivers ~28), so trusting it causes the synthesized timeline to drift + * relative to real time, which makes the sink consider frames late and + * skip pacing entirely. Measuring the real rate keeps PTS in step with + * arrival on average, so the sink can pace inside bursts while still + * tracking the printer's actual frame cadence. + */ + if (src->avg_period == 0) { + int fps = src->frame_rate > 0 ? src->frame_rate : 30; + src->avg_period = GST_SECOND / fps; } + if (running_now != GST_CLOCK_TIME_NONE && src->last_arrival != 0) { + GstClockTimeDiff delta = GST_CLOCK_DIFF(src->last_arrival, running_now); + /* clamp to plausible video frame periods (5..200 ms) so a one-off + * burst-of-zero or long stall doesn't poison the average */ + if (delta > 5 * GST_MSECOND && delta < 200 * GST_MSECOND) { + src->avg_period = (src->avg_period * 15 + (GstClockTime)delta) / 16; + } + } + src->last_arrival = (running_now != GST_CLOCK_TIME_NONE) ? running_now : src->last_arrival; + GstClockTime period = src->avg_period; + + /* Lead time: schedule frames a few periods in the future of their + * arrival, so the sink has a small jitter buffer. Without this, frames + * arriving slightly later than expected land behind the running clock + * and the sink renders them immediately, producing visible stutter. + * 100ms is invisible for a live print-monitor view. + */ + const GstClockTime LEAD = 100 * GST_MSECOND; + + if (!src->sttime) { + src->sttime = (running_now != GST_CLOCK_TIME_NONE) ? running_now + LEAD : LEAD; + src->frame_count = 0; + } + + GstClockTime pts = src->sttime + src->frame_count * period; + + /* Safety net: with the lead applied, expected drift is roughly -LEAD + * (pts sits LEAD ns ahead of running_now). Re-anchor only if the + * synthesized timeline diverges from that expectation by several frame + * periods, which indicates a real disturbance (printer paused, stream + * resumed, large fps change) rather than ordinary jitter. + */ + if (running_now != GST_CLOCK_TIME_NONE) { + GstClockTimeDiff drift = GST_CLOCK_DIFF(pts, running_now); + GstClockTimeDiff expected = -(GstClockTimeDiff)LEAD; + GstClockTimeDiff slack = (GstClockTimeDiff)(4 * period); + if (drift > expected + slack || drift < expected - slack) { + GST_DEBUG_OBJECT(src, "ts drift %" G_GINT64_FORMAT " ns; re-anchoring", drift); + src->sttime = running_now + LEAD; + src->frame_count = 0; + pts = src->sttime; + } + } + + GST_BUFFER_PTS(*outbuf) = pts; + GST_BUFFER_DTS(*outbuf) = pts; + GST_BUFFER_DURATION(*outbuf) = period; + src->frame_count++; GST_DEBUG_OBJECT(src, "sttime:%lu, DTS:%lu, PTS: %lu~", src->sttime, GST_BUFFER_DTS(*outbuf), GST_BUFFER_PTS(*outbuf)); @@ -396,12 +448,16 @@ gst_bambusrc_start (GstBaseSrc * bsrc) GST_INFO_OBJECT (src, "stream %d type=%d, sub_type=%d", i, info.type, info.sub_type); if (info.type == VIDE) { src->video_type = info.sub_type; + src->frame_rate = info.format.video.frame_rate; GST_INFO_OBJECT (src, " width %d height=%d, frame_rate=%d", info.format.video.width, info.format.video.height, info.format.video.frame_rate); } } src->sttime = 0; + src->frame_count = 0; + src->last_arrival = 0; + src->avg_period = 0; return TRUE; } diff --git a/src/slic3r/GUI/Printer/gstbambusrc.h b/src/slic3r/GUI/Printer/gstbambusrc.h index fbffb2df03..d5c022a40e 100644 --- a/src/slic3r/GUI/Printer/gstbambusrc.h +++ b/src/slic3r/GUI/Printer/gstbambusrc.h @@ -65,6 +65,10 @@ struct _GstBambuSrc Bambu_Tunnel tnl; GstClockTime sttime; int video_type; + int frame_rate; + guint64 frame_count; + GstClockTime last_arrival; + GstClockTime avg_period; }; extern void gstbambusrc_register();