From b0ada2dee57d5614be8ffea6f97de3ca606be6a7 Mon Sep 17 00:00:00 2001 From: Ian Chua Date: Wed, 9 Sep 2026 13:40:51 +0800 Subject: [PATCH] fix: ffmpeg http camera stream jittering due to incomplete frames --- src/slic3r/GUI/AVVideoDecoder.hpp | 8 +++ src/slic3r/GUI/MediaPlayCtrl.cpp | 39 +++++++++- src/slic3r/GUI/wxMediaCtrl3.cpp | 114 ++++++++++++++++++++++++++---- 3 files changed, 146 insertions(+), 15 deletions(-) diff --git a/src/slic3r/GUI/AVVideoDecoder.hpp b/src/slic3r/GUI/AVVideoDecoder.hpp index 4277734e08..d3d16a11c1 100644 --- a/src/slic3r/GUI/AVVideoDecoder.hpp +++ b/src/slic3r/GUI/AVVideoDecoder.hpp @@ -36,6 +36,14 @@ public: bool toWxBitmap(wxBitmap &bitmap, wxSize const & size); + // Native size of the most recently decoded frame, or an unspecified size if + // nothing has decoded yet. Lets a caller learn the video dimensions when the + // container/probe could not report them up front. + wxSize decoded_frame_size() const + { + return got_frame_ && frame_ ? wxSize{frame_->width, frame_->height} : wxSize{}; + } + private: AVCodecContext *codec_ctx_ = nullptr; AVFrame * frame_ = nullptr; diff --git a/src/slic3r/GUI/MediaPlayCtrl.cpp b/src/slic3r/GUI/MediaPlayCtrl.cpp index 42270252a9..69576666e6 100644 --- a/src/slic3r/GUI/MediaPlayCtrl.cpp +++ b/src/slic3r/GUI/MediaPlayCtrl.cpp @@ -10,6 +10,8 @@ #include "slic3r/Utils/BBLNetworkPlugin.hpp" +#include + #include #include #include @@ -166,8 +168,10 @@ void MediaPlayCtrl::SetMachineObject(MachineObject* obj) { const CameraStreamMode mode = current_mode(); if (mode != m_last_mode) { - if (m_last_state != MEDIASTATE_IDLE) + if (m_last_state != MEDIASTATE_IDLE) { + m_failed_code = 0; // a mode switch is not a stream failure - don't arm back-off Stop(" "); + } m_last_mode = mode; } @@ -189,7 +193,12 @@ void MediaPlayCtrl::SetMachineObject(MachineObject* obj) Play(); return; } + // A genuine machine/URL switch: not a failure, so drop any pending + // failure back-off before (re)starting on the new target. m_web_user_stopped = false; + m_failed_code = 0; + m_failed_retry = 0; + m_next_retry = wxDateTime(); if (m_last_state != MEDIASTATE_IDLE) Stop(" "); if (IsEnabled()) @@ -552,19 +561,43 @@ void MediaPlayCtrl::Stop(wxString const &msg, wxString const &msg2) } switch (current_mode()) { case CameraStreamMode::http: - case CameraStreamMode::http_snapshot: + case CameraStreamMode::http_snapshot: { + const bool snapshot = current_mode() == CameraStreamMode::http_snapshot; if (m_last_state != MEDIASTATE_IDLE) { - if (m_web_ctrl) m_web_ctrl->Stop(); + if (snapshot) { + if (m_web_ctrl) m_web_ctrl->Stop(); + } else { + // http mode plays through the ffmpeg backend (m_media_ctrl), not + // the webview - tear its read thread down too, otherwise it keeps + // pulling and painting frames after the UI says "Video Stopped". + boost::unique_lock lock(m_mutex); + m_tasks.push_back(""); + m_cond.notify_all(); + } m_button_play->SetIcon("media_play"); m_last_state = MEDIASTATE_IDLE; if (!msg.IsEmpty()) SetStatus(msg); else SetStatus(_L("Video Stopped."), false); + // SetMachineObject re-drives Play() on every device refresh (~1s). + // This branch returns before the legacy back-off below, so on a real + // failure it has to arm m_next_retry itself or the stream restarts + // once a second forever. Escalate 5s..30s; m_failed_retry is cleared + // on success (onStateChanged) and on a deliberate switch + // (SetMachineObject), and a manual play via TogglePlay resets both. + if (m_failed_code != 0) { + const bool auto_retry = wxGetApp().app_config->get("liveview", "auto_retry") != "false"; + ++m_failed_retry; + m_next_retry = auto_retry + ? wxDateTime::Now() + wxTimeSpan::Seconds(std::min(5 * m_failed_retry, 30)) + : wxDateTime::Now() + wxTimeSpan::Days(1); // "off": wait for a manual retry + } } else if (!msg.IsEmpty()) { SetStatus(msg, false); } return; + } default: break; } diff --git a/src/slic3r/GUI/wxMediaCtrl3.cpp b/src/slic3r/GUI/wxMediaCtrl3.cpp index 83a2b780a5..c04b27b118 100644 --- a/src/slic3r/GUI/wxMediaCtrl3.cpp +++ b/src/slic3r/GUI/wxMediaCtrl3.cpp @@ -4,8 +4,13 @@ #include "libslic3r/Utils.hpp" #include #include +#include +#include +#include +#include extern "C" { #include +#include } #ifdef __WIN32__ #include @@ -250,6 +255,58 @@ void wxMediaCtrl3::bambu_log(void *ctx, int level, tchar const *msg2) BOOST_LOG_TRIVIAL(info) << msg.ToUTF8().data(); } +// FFmpeg's own diagnostics (HTTP status, "Invalid data found", demuxer choice, +// missing stream dimensions, ...) are otherwise swallowed: a failed camera open +// only surfaces as wxMediaCtrl3's generic error code, which MediaPlayCtrl maps to +// the misleading "Player is malfunctioning" string. Forward them to the Orca log +// instead. Verbosity defaults to AV_LOG_VERBOSE and can be raised at runtime with +// ORCA_FFMPEG_LOG_LEVEL=debug|trace|... (or lowered to warning/error/quiet). +static int ffmpeg_log_level_from_env() +{ + const char *env = std::getenv("ORCA_FFMPEG_LOG_LEVEL"); + if (env == nullptr || *env == '\0') + return AV_LOG_VERBOSE; + const wxString v = wxString(env).Lower(); + if (v == "quiet") return AV_LOG_QUIET; + if (v == "panic") return AV_LOG_PANIC; + if (v == "fatal") return AV_LOG_FATAL; + if (v == "error") return AV_LOG_ERROR; + if (v == "warning") return AV_LOG_WARNING; + if (v == "info") return AV_LOG_INFO; + if (v == "verbose") return AV_LOG_VERBOSE; + if (v == "debug") return AV_LOG_DEBUG; + if (v == "trace") return AV_LOG_TRACE; + return AV_LOG_VERBOSE; +} + +static void ffmpeg_log_callback(void *avcl, int level, const char *fmt, va_list vl) +{ + if (level > av_log_get_level()) + return; + thread_local int print_prefix = 1; + char line[1024]; + av_log_format_line2(avcl, level, fmt, vl, line, (int) sizeof(line), &print_prefix); + size_t len = std::strlen(line); + while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r' || line[len - 1] == ' ')) + line[--len] = '\0'; + if (len == 0) + return; + if (level <= AV_LOG_ERROR) + BOOST_LOG_TRIVIAL(error) << "ffmpeg: " << line; + else if (level <= AV_LOG_WARNING) + BOOST_LOG_TRIVIAL(warning) << "ffmpeg: " << line; + else if (level <= AV_LOG_INFO) + BOOST_LOG_TRIVIAL(info) << "ffmpeg: " << line; + else + BOOST_LOG_TRIVIAL(debug) << "ffmpeg: " << line; +} + +static void install_ffmpeg_logger() +{ + av_log_set_level(ffmpeg_log_level_from_env()); + av_log_set_callback(&ffmpeg_log_callback); +} + int wxMediaCtrl3::ffmpeg_interrupt_callback(void *opaque) { auto *ctrl = static_cast(opaque); @@ -259,6 +316,9 @@ int wxMediaCtrl3::ffmpeg_interrupt_callback(void *opaque) int wxMediaCtrl3::PlayFfmpeg(std::shared_ptr const &url, std::unique_lock &lock) { + static std::once_flag logger_once; + std::call_once(logger_once, install_ffmpeg_logger); + if (avformat_network_init() < 0) return 2; @@ -287,13 +347,22 @@ int wxMediaCtrl3::PlayFfmpeg(std::shared_ptr const &url, std::unique_lock const bool http_stream = scheme.CmpNoCase("http") == 0 || scheme.CmpNoCase("https") == 0; AVDictionary *options = nullptr; if (http_stream) { - // This is a live multipart MJPEG stream. Keep FFmpeg from building a - // read-ahead buffer, otherwise the UI can display frames several - // seconds behind the camera. + // Live multipart MJPEG. fflags=nobuffer / AVFMT_FLAG_NOBUFFER / max_delay=0 + // (set above) are the low-latency levers - they disable the demuxer + // read-ahead queue. probesize / analyzeduration only bound the one-off + // avformat_find_stream_info() at open; a 32-byte budget returned before a + // whole JPEG frame was seen, so width/height came back unset and the open + // was rejected. Give it room to identify one frame (a startup cost only). + // rw_timeout / timeout bound a wedged connect or read so a stale stream + // fails fast and is retried, instead of the reader thread hanging. + // avioflags=direct is deliberately NOT set: unbuffered reads make the + // mpjpeg demuxer emit "Packet corrupt" and bail on any short read across + // a multipart boundary. av_dict_set(&options, "fflags", "nobuffer", 0); - av_dict_set(&options, "avioflags", "direct", 0); - av_dict_set(&options, "probesize", "32", 0); - av_dict_set(&options, "analyzeduration", "0", 0); + av_dict_set(&options, "probesize", "5000000", 0); + av_dict_set(&options, "analyzeduration", "1000000", 0); + av_dict_set(&options, "rw_timeout", "5000000", 0); + av_dict_set(&options, "timeout", "5000000", 0); } else { av_dict_set(&options, "rtsp_transport", "tcp", 0); } @@ -318,12 +387,21 @@ int wxMediaCtrl3::PlayFfmpeg(std::shared_ptr const &url, std::unique_lock if (decoder.open(*format_context->streams[video_stream]->codecpar) < 0) return finish(2); - m_video_size = {format_context->streams[video_stream]->codecpar->width, - format_context->streams[video_stream]->codecpar->height}; - if (!m_video_size.IsFullySpecified() || m_video_size.x <= 0 || m_video_size.y <= 0) - return finish(2); - adjust_frame_size(m_frame_size, m_video_size, GetSize()); - NotifyStopped(); + // Prefer the dimensions the container reported. A small probe budget, or a + // camera that doesn't announce a size up front, can leave these unset - in + // that case fill them in from the first frame that decodes (below) rather + // than failing the open outright. + auto apply_video_size = [&](wxSize size) { + if (!size.IsFullySpecified() || size.x <= 0 || size.y <= 0) + return false; + m_video_size = size; + adjust_frame_size(m_frame_size, m_video_size, GetSize()); + NotifyStopped(); + return true; + }; + bool have_size = apply_video_size({format_context->streams[video_stream]->codecpar->width, + format_context->streams[video_stream]->codecpar->height}); + int size_probe_frames = 0; // frames spent still waiting for a usable size AVPacket *packet = av_packet_alloc(); if (!packet) @@ -340,6 +418,18 @@ int wxMediaCtrl3::PlayFfmpeg(std::shared_ptr const &url, std::unique_lock if (packet->stream_index == video_stream) { const int decode_error = decoder.decode(*packet); if (decode_error == 0) { + if (!have_size) { + have_size = apply_video_size(decoder.decoded_frame_size()); + if (!have_size) { + av_packet_unref(packet); + // MJPEG yields a sized frame on the first full packet; if + // several seconds of frames never do, treat it as a bad + // stream instead of sitting in "Loading..." forever. + if (++size_probe_frames > 120) + break; + continue; + } + } auto frame_size = m_frame_size; lock.unlock(); #ifdef _WIN32