diff --git a/CMakeLists.txt b/CMakeLists.txt index 4f5ccbeb44..8b954ef753 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1067,6 +1067,7 @@ function(orcaslicer_copy_dlls target config postfix output_dlls) ${CMAKE_PREFIX_PATH}/bin/occt/TKXDESTEP.dll ${CMAKE_PREFIX_PATH}/bin/occt/TKXSBase.dll ${CMAKE_PREFIX_PATH}/bin/freetype.dll + ${CMAKE_PREFIX_PATH}/bin/avformat-61.dll ${CMAKE_PREFIX_PATH}/bin/avcodec-61.dll ${CMAKE_PREFIX_PATH}/bin/swresample-5.dll ${CMAKE_PREFIX_PATH}/bin/swscale-8.dll @@ -1106,6 +1107,7 @@ function(orcaslicer_copy_dlls target config postfix output_dlls) ${_out_dir}/TKXSBase.dll ${_out_dir}/freetype.dll + ${_out_dir}/avformat-61.dll ${_out_dir}/avcodec-61.dll ${_out_dir}/swresample-5.dll ${_out_dir}/swscale-8.dll @@ -1128,7 +1130,10 @@ function(orcaslicer_copy_sos target config postfix output_sos) set(_out_dir "${CMAKE_CURRENT_BINARY_DIR}") endif () - file(COPY ${CMAKE_PREFIX_PATH}/lib/libavcodec.so + file(COPY ${CMAKE_PREFIX_PATH}/lib/libavformat.so + ${CMAKE_PREFIX_PATH}/lib/libavformat.so.61 + ${CMAKE_PREFIX_PATH}/lib/libavformat.so.61.1.100 + ${CMAKE_PREFIX_PATH}/lib/libavcodec.so ${CMAKE_PREFIX_PATH}/lib/libavcodec.so.61 ${CMAKE_PREFIX_PATH}/lib/libavcodec.so.61.3.100 ${CMAKE_PREFIX_PATH}/lib/libavutil.so @@ -1143,6 +1148,9 @@ function(orcaslicer_copy_sos target config postfix output_sos) DESTINATION ${_out_dir}) set(${output_sos} + ${_out_dir}/libavformat.so + ${_out_dir}/libavformat.so.61 + ${_out_dir}/libavformat.so.61.1.100 ${_out_dir}/libavcodec.so ${_out_dir}/libavcodec.so.61 ${_out_dir}/libavcodec.so.61.3.100 @@ -1271,6 +1279,8 @@ endif () if (CMAKE_SYSTEM_NAME STREQUAL "Linux") set(LIBRARY_FILES + ${LIBDIR_BIN}/libavformat.so.61 + ${LIBDIR_BIN}/libavformat.so.61.1.100 ${LIBDIR_BIN}/libavcodec.so.61 ${LIBDIR_BIN}/libavcodec.so.61.3.100 ${LIBDIR_BIN}/libavutil.so.59 diff --git a/deps/FFMPEG/FFMPEG.cmake b/deps/FFMPEG/FFMPEG.cmake index 38e952bbdd..3c582f0682 100644 --- a/deps/FFMPEG/FFMPEG.cmake +++ b/deps/FFMPEG/FFMPEG.cmake @@ -69,14 +69,14 @@ else () --disable-filters --enable-filter=*null*,afade,*fifo,*format,*resample,aeval,allrgb,allyuv,atempo,pan,*bars,color,*key,crop,draw*,eq*,framerate,*_qsv,*_vaapi,*v4l2*,hw*,scale,volume,test* --disable-protocols - --enable-protocol=file,fd,pipe,rtp,udp + --enable-protocol=file,fd,pipe,rtp,tcp,udp --disable-muxers --enable-muxer=rtp --disable-encoders --disable-decoders --enable-decoder=*aac*,h264*,mp3*,mjpeg,rv* --disable-demuxers - --enable-demuxer=h264,mp3,mov + --enable-demuxer=h264,mp3,mov,rtsp,sdp --disable-zlib --disable-avdevice BUILD_IN_SOURCE ON diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 140b1cec39..8308161a7b 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -222,6 +222,7 @@ set(SLIC3R_GUI_SOURCES GUI/GLToolbar.hpp GUI/ImageDPIFrame.cpp GUI/ImageDPIFrame.hpp + GUI/IMediaController.hpp GUI/GUI_App.cpp GUI/GUI_App.hpp GUI/GUI_AuxiliaryList.cpp @@ -530,6 +531,8 @@ set(SLIC3R_GUI_SOURCES GUI/WebUserLoginDialog.hpp GUI/WebViewDialog.cpp GUI/WebViewDialog.hpp + GUI/WebMediaController.hpp + GUI/WebMediaController.cpp GUI/Widgets/AMSControl.cpp GUI/Widgets/AMSControl.hpp GUI/Widgets/AMSItem.cpp @@ -906,14 +909,15 @@ endif () if (APPLE) # Static FFmpeg from the deps install: nothing to bundle into the .app, - # no rpath/install_name handling. Order matters: avcodec -> swscale -> avutil. + # no rpath/install_name handling. Order matters: avformat -> avcodec -> swscale -> avutil. + find_library(LIBAVFORMAT_LIBRARY NAMES libavformat.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH) find_library(LIBAVCODEC_LIBRARY NAMES libavcodec.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH) find_library(LIBSWSCALE_LIBRARY NAMES libswscale.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH) find_library(LIBAVUTIL_LIBRARY NAMES libavutil.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH) - if (NOT LIBAVCODEC_LIBRARY OR NOT LIBSWSCALE_LIBRARY OR NOT LIBAVUTIL_LIBRARY) - message(FATAL_ERROR "Static FFmpeg (libavcodec.a/libswscale.a/libavutil.a) not found under ${CMAKE_PREFIX_PATH}/lib. Rebuild the deps — FFMPEG builds static-only on macOS.") + if (NOT LIBAVFORMAT_LIBRARY OR NOT LIBAVCODEC_LIBRARY OR NOT LIBSWSCALE_LIBRARY OR NOT LIBAVUTIL_LIBRARY) + message(FATAL_ERROR "Static FFmpeg (libavformat.a/libavcodec.a/libswscale.a/libavutil.a) not found under ${CMAKE_PREFIX_PATH}/lib. Rebuild the deps — FFMPEG builds static-only on macOS.") endif () - target_link_libraries(libslic3r_gui ${LIBAVCODEC_LIBRARY} ${LIBSWSCALE_LIBRARY} ${LIBAVUTIL_LIBRARY}) + target_link_libraries(libslic3r_gui ${LIBAVFORMAT_LIBRARY} ${LIBAVCODEC_LIBRARY} ${LIBSWSCALE_LIBRARY} ${LIBAVUTIL_LIBRARY}) target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_PREFIX_PATH}/include) elseif (WIN32) # Prebuilt shared FFmpeg from the deps install. Windows has no pkg-config, @@ -929,6 +933,7 @@ elseif (WIN32) target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_PREFIX_PATH}/include) else () pkg_check_modules(LIBAV REQUIRED IMPORTED_TARGET + libavformat libavcodec libswscale libavutil diff --git a/src/slic3r/GUI/AVVideoDecoder.cpp b/src/slic3r/GUI/AVVideoDecoder.cpp index d7b8432bd3..76965fedef 100644 --- a/src/slic3r/GUI/AVVideoDecoder.cpp +++ b/src/slic3r/GUI/AVVideoDecoder.cpp @@ -45,6 +45,25 @@ int AVVideoDecoder::open(Bambu_StreamInfo const &info) return 0; } +int AVVideoDecoder::open(AVCodecParameters const ¶meters) +{ + if (avcodec_parameters_to_context(codec_ctx_, ¶meters) < 0) + return -1; + + auto codec = avcodec_find_decoder(codec_ctx_->codec_id); + if (codec == nullptr) { + fprintf(stderr, "AVVideoDecoder: unsupported codec!\n"); + return -1; + } + if (avcodec_open2(codec_ctx_, codec, nullptr) < 0) { + fprintf(stderr, "AVVideoDecoder: could not open codec\n"); + return -1; + } + + frame_ = av_frame_alloc(); + return frame_ == nullptr ? -1 : 0; +} + int AVVideoDecoder::decode(const Bambu_Sample &sample) { int ret = -1; @@ -71,6 +90,14 @@ int AVVideoDecoder::decode(const Bambu_Sample &sample) return ret; } +int AVVideoDecoder::decode(const AVPacket &packet) +{ + int ret = avcodec_send_packet(codec_ctx_, &packet); + if (ret == 0) + got_frame_ = avcodec_receive_frame(codec_ctx_, frame_) == 0; + return ret; +} + int AVVideoDecoder::flush() { int ret = avcodec_send_packet(codec_ctx_, nullptr); diff --git a/src/slic3r/GUI/AVVideoDecoder.hpp b/src/slic3r/GUI/AVVideoDecoder.hpp index 4111e860a2..4277734e08 100644 --- a/src/slic3r/GUI/AVVideoDecoder.hpp +++ b/src/slic3r/GUI/AVVideoDecoder.hpp @@ -23,8 +23,10 @@ public: public: int open(Bambu_StreamInfo const &info); + int open(AVCodecParameters const ¶meters); int decode(Bambu_Sample const &sample); + int decode(AVPacket const &packet); int flush(); diff --git a/src/slic3r/GUI/CameraPopup.cpp b/src/slic3r/GUI/CameraPopup.cpp index 0ee5694a34..f0b71cb7bf 100644 --- a/src/slic3r/GUI/CameraPopup.cpp +++ b/src/slic3r/GUI/CameraPopup.cpp @@ -28,7 +28,6 @@ wxEND_EVENT_TABLE() wxDEFINE_EVENT(EVT_VCAMERA_SWITCH, wxMouseEvent); wxDEFINE_EVENT(EVT_SDCARD_ABSENT_HINT, wxCommandEvent); -wxDEFINE_EVENT(EVT_CAM_SOURCE_CHANGE, wxCommandEvent); #define CAMERAPOPUP_CLICK_INTERVAL 20 @@ -102,34 +101,6 @@ CameraPopup::CameraPopup(wxWindow *parent) top_sizer->Add(0, 0, wxALL, 0); } - // Orca: custom IP camera source — lets the user point Live Video at any camera URL (Orca feature; not in the reference) - m_custom_camera_input_confirm = new Button(m_panel, _L("Enable")); - m_custom_camera_input_confirm->SetBackgroundColor(wxColour(38, 166, 154)); - m_custom_camera_input_confirm->SetBorderColor(wxColour(38, 166, 154)); - m_custom_camera_input_confirm->SetTextColor(wxColour(0xFFFFFE)); - m_custom_camera_input_confirm->SetFont(Label::Body_14); - m_custom_camera_input_confirm->SetMinSize(wxSize(FromDIP(90), FromDIP(30))); - m_custom_camera_input_confirm->SetPosition(wxDefaultPosition); - m_custom_camera_input_confirm->SetCornerRadius(FromDIP(12)); - m_custom_camera_input = new TextInput(m_panel, wxEmptyString, wxEmptyString, wxEmptyString, wxDefaultPosition, wxDefaultSize); - m_custom_camera_input->GetTextCtrl()->SetHint(_L("Hostname or IP")); - m_custom_camera_input->GetTextCtrl()->SetFont(Label::Body_14); - m_custom_camera_hint = new wxStaticText(m_panel, wxID_ANY, _L("Custom camera source")); - m_custom_camera_hint->Wrap(-1); - m_custom_camera_hint->SetFont(Label::Head_14); - m_custom_camera_hint->SetForegroundColour(TEXT_COL); - - m_custom_camera_input_confirm->Bind(wxEVT_BUTTON, &CameraPopup::on_camera_source_changed, this); - - if (!wxGetApp().app_config->get("camera", "custom_source").empty()) { - m_custom_camera_input->GetTextCtrl()->SetValue(wxGetApp().app_config->get("camera", "custom_source")); - set_custom_cam_button_state(wxGetApp().app_config->get("camera", "enable_custom_source") == "true"); - } - - top_sizer->Add(m_custom_camera_hint, 0, wxALIGN_CENTER_VERTICAL | wxALIGN_LEFT | wxALL, FromDIP(5)); - top_sizer->Add(0, 0, wxALL, 0); - top_sizer->Add(m_custom_camera_input, 2, wxALIGN_CENTER_VERTICAL | wxEXPAND | wxALL, FromDIP(5)); - top_sizer->Add(m_custom_camera_input_confirm, 1, wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT | wxALL, FromDIP(5)); main_sizer->Add(top_sizer, 0, wxALL, FromDIP(10)); auto url = wxString(L"https://www.orcaslicer.com/wiki/"); // Orca: neutral wiki link (vendor URL removed) @@ -184,37 +155,6 @@ void CameraPopup::sdcard_absent_hint() GetEventHandler()->ProcessEvent(evt); } -void CameraPopup::on_camera_source_changed(wxCommandEvent &event) -{ - if (m_obj && !m_custom_camera_input->GetTextCtrl()->IsEmpty()) { - handle_camera_source_change(); - } -} - -void CameraPopup::handle_camera_source_change() -{ - m_custom_camera_enabled = !m_custom_camera_enabled; - - set_custom_cam_button_state(m_custom_camera_enabled); - - wxGetApp().app_config->set("camera", "custom_source", m_custom_camera_input->GetTextCtrl()->GetValue().ToStdString()); - wxGetApp().app_config->set("camera", "enable_custom_source", m_custom_camera_enabled); - - wxCommandEvent evt(EVT_CAM_SOURCE_CHANGE); - evt.SetEventObject(this); - GetEventHandler()->ProcessEvent(evt); -} - -void CameraPopup::set_custom_cam_button_state(bool state) -{ - m_custom_camera_enabled = state; - auto stateColour = state ? wxColour(170, 0, 0) : wxColour(38, 166, 154); - auto stateText = state ? "Disable" : "Enable"; - m_custom_camera_input_confirm->SetBackgroundColor(stateColour); - m_custom_camera_input_confirm->SetBorderColor(stateColour); - m_custom_camera_input_confirm->SetLabel(_L(stateText)); -} - void CameraPopup::on_switch_recording(wxCommandEvent& event) { if (!m_obj) return; diff --git a/src/slic3r/GUI/CameraPopup.hpp b/src/slic3r/GUI/CameraPopup.hpp index dbdb81a9cb..5295407e8f 100644 --- a/src/slic3r/GUI/CameraPopup.hpp +++ b/src/slic3r/GUI/CameraPopup.hpp @@ -15,14 +15,12 @@ #include "Widgets/SwitchButton.hpp" #include "Widgets/RadioBox.hpp" #include "Widgets/PopupWindow.hpp" -#include "Widgets/TextInput.hpp" namespace Slic3r { namespace GUI { wxDECLARE_EVENT(EVT_VCAMERA_SWITCH, wxMouseEvent); wxDECLARE_EVENT(EVT_SDCARD_ABSENT_HINT, wxCommandEvent); -wxDECLARE_EVENT(EVT_CAM_SOURCE_CHANGE, wxCommandEvent); class CameraPopup : public PopupWindow { @@ -53,9 +51,6 @@ protected: void on_switch_recording(wxCommandEvent& event); void on_set_resolution(); void sdcard_absent_hint(); - void on_camera_source_changed(wxCommandEvent& event); - void handle_camera_source_change(); - void set_custom_cam_button_state(bool state); wxWindow * create_item_radiobox(wxString title, wxWindow *parent, wxString tooltip, int padding_left); void select_curr_radiobox(int btn_idx); @@ -74,10 +69,6 @@ private: SwitchButton* m_switch_vcamera; wxStaticText* m_text_liveview_retry; SwitchButton* m_switch_liveview_retry; - wxStaticText* m_custom_camera_hint; - TextInput* m_custom_camera_input; - Button* m_custom_camera_input_confirm; - bool m_custom_camera_enabled{ false }; wxStaticText* m_text_resolution; wxWindow* m_resolution_options[RESOLUTION_OPTIONS_NUM]; wxScrolledWindow *m_panel; diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp index 06f574c8a2..741ed788fc 100644 --- a/src/slic3r/GUI/DeviceManager.cpp +++ b/src/slic3r/GUI/DeviceManager.cpp @@ -1338,7 +1338,6 @@ int MachineObject::command_get_access_code() { return this->publish_json(j); } - int MachineObject::command_request_push_all(bool request_now) { auto curr_time = std::chrono::system_clock::now(); diff --git a/src/slic3r/GUI/DeviceManager.hpp b/src/slic3r/GUI/DeviceManager.hpp index 6df11d10f5..8788c2288d 100644 --- a/src/slic3r/GUI/DeviceManager.hpp +++ b/src/slic3r/GUI/DeviceManager.hpp @@ -749,6 +749,7 @@ public: int command_set_printer_nozzle(std::string nozzle_type, float diameter); int command_set_printer_nozzle2(int id, std::string nozzle_type, float diameter); int command_get_access_code(); + int command_start_camera(); int command_ack_proceed(json& proceed); int command_purification_disable(); int command_dont_remind_next_time(json& mqtt_guard_json); @@ -797,7 +798,6 @@ public: int command_ams_select_tray(std::string tray_id); int command_ams_refresh_rfid(std::string tray_id); int command_ams_refresh_rfid2(int ams_id, int slot_id); - int command_start_camera(); int command_ams_control(std::string action); int command_ams_drying_stop(); int command_start_extrusion_cali(int tray_index, int nozzle_temp, int bed_temp, float max_volumetric_speed, std::string setting_id = ""); diff --git a/src/slic3r/GUI/IMediaController.hpp b/src/slic3r/GUI/IMediaController.hpp new file mode 100644 index 0000000000..982157e5bc --- /dev/null +++ b/src/slic3r/GUI/IMediaController.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include +#include + +#include + +namespace Slic3r { namespace GUI { + +class IMediaController +{ +public: + virtual void Load(wxURI url) = 0; + + // The default keeps existing media controllers unaware of camera-specific modes. + virtual void Load(wxURI url, CameraStreamMode mode) + { + (void) mode; + Load(url); + } + + virtual void Play() = 0; + + virtual void Stop() = 0; + + virtual wxMediaState GetState() { return wxMediaState{}; } + + virtual int GetLastError() const { return {}; }; + + virtual wxSize GetVideoSize() const { return {}; }; + +private: +}; + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/MediaPlayCtrl.cpp b/src/slic3r/GUI/MediaPlayCtrl.cpp index 707c7f853c..77a183f090 100644 --- a/src/slic3r/GUI/MediaPlayCtrl.cpp +++ b/src/slic3r/GUI/MediaPlayCtrl.cpp @@ -146,8 +146,48 @@ MediaPlayCtrl::~MediaPlayCtrl() BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": " << this; } +void MediaPlayCtrl::SetWebMediaController(IMediaController *ctrl) +{ + m_web_ctrl = ctrl; +} + +CameraStreamMode MediaPlayCtrl::current_mode() const +{ + auto agent = wxGetApp().getAgent(); + return agent ? agent->get_camera_stream_mode() : CameraStreamMode::none; +} + void MediaPlayCtrl::SetMachineObject(MachineObject* obj) { + switch (current_mode()) { + case CameraStreamMode::http: + case CameraStreamMode::http_snapshot: + case CameraStreamMode::rtsp: { + std::string machine = obj ? obj->get_dev_id() : ""; + auto agent = wxGetApp().getAgent(); + std::string url = agent ? agent->get_local_camera_stream_url() : ""; + m_camera_exists = !url.empty(); + Enable(obj && m_camera_exists); + bool changed = machine != m_machine || url != m_agent_camera_url; + m_machine = machine; + m_agent_camera_url = url; + m_url = from_u8(url); + if (!changed) { + if (m_last_state == MEDIASTATE_IDLE && IsEnabled() && !m_web_user_stopped) + Play(); + return; + } + m_web_user_stopped = false; + if (m_last_state != MEDIASTATE_IDLE) + Stop(" "); + if (IsEnabled()) + Play(); + return; + } + default: + break; + } + std::string machine = obj ? obj->get_dev_id() : ""; if (obj) { m_camera_exists = obj->has_ipcam; @@ -249,6 +289,42 @@ void refresh_agora_url(char const* device, char const* dev_ver, char const* chan void MediaPlayCtrl::Play() { + switch (current_mode()) { + case CameraStreamMode::http: + case CameraStreamMode::http_snapshot: + if (!m_next_retry.IsValid() || wxDateTime::Now() < m_next_retry) + return; + if (!IsShownOnScreen()) return; + if (m_last_state != MEDIASTATE_IDLE) return; + if (m_machine.empty() || !IsEnabled() || !m_camera_exists || m_url.IsEmpty() || !m_web_ctrl) { + Stop(_L("Please confirm if the printer is connected.")); + return; + } + if (auto agent = wxGetApp().getAgent()) + agent->command_start_camera(m_machine); + m_button_play->SetIcon("media_stop"); + m_web_ctrl->Load(wxURI(m_url), current_mode()); + m_web_ctrl->Play(); + m_last_state = wxMEDIASTATE_PLAYING; + SetStatus(_L("Playing..."), false); + return; + case CameraStreamMode::rtsp: + if (m_next_retry.IsValid() && wxDateTime::Now() < m_next_retry) + return; + if (!IsShownOnScreen()) return; + if (m_last_state != MEDIASTATE_IDLE) return; + m_failed_code = 0; + if (m_machine.empty() || !IsEnabled() || !m_camera_exists || m_url.IsEmpty()) { + Stop(_L("Please confirm if the printer is connected.")); + return; + } + m_button_play->SetIcon("media_stop"); + load(); + return; + default: + break; + } + if (!m_next_retry.IsValid() || wxDateTime::Now() < m_next_retry) return; if (!IsShownOnScreen()) @@ -376,8 +452,38 @@ void MediaPlayCtrl::Play() void start_ping_test(); +void MediaPlayCtrl::StopWebStream() +{ + if (m_last_state == MEDIASTATE_IDLE) + return; + if (m_web_ctrl) + m_web_ctrl->Stop(); + m_button_play->SetIcon("media_play"); + m_last_state = MEDIASTATE_IDLE; + SetStatus(_L("Video Stopped."), false); +} + void MediaPlayCtrl::Stop(wxString const &msg, wxString const &msg2) { + switch (current_mode()) { + case CameraStreamMode::http: + case CameraStreamMode::http_snapshot: + if (m_last_state != MEDIASTATE_IDLE) { + if (m_web_ctrl) m_web_ctrl->Stop(); + m_button_play->SetIcon("media_play"); + m_last_state = MEDIASTATE_IDLE; + if (!msg.IsEmpty()) + SetStatus(msg); + else + SetStatus(_L("Video Stopped."), false); + } else if (!msg.IsEmpty()) { + SetStatus(msg, false); + } + return; + default: + break; + } + int last_state = m_last_state; if (m_last_state != MEDIASTATE_IDLE) { @@ -454,10 +560,12 @@ void MediaPlayCtrl::TogglePlay() BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::TogglePlay"; if (m_last_state != MEDIASTATE_IDLE) { m_next_retry = wxDateTime(); + m_web_user_stopped = true; Stop(); } else { m_failed_retry = 0; m_user_triggered = true; + m_web_user_stopped = false; if (m_last_user_play + wxTimeSpan::Minutes(5) < wxDateTime::Now()) { m_last_failed_codes.clear(); m_last_user_play = wxDateTime::Now(); @@ -661,7 +769,7 @@ void MediaPlayCtrl::load() { m_last_state = MEDIASTATE_LOADING; SetStatus(_L("Loading...")); - if (wxGetApp().app_config->get("internal_developer_mode") == "true") { + if (current_mode() != CameraStreamMode::rtsp) { std::string file_h264 = data_dir() + "/video.h264"; std::string file_info = data_dir() + "/video.info"; BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl dump video to " << file_h264; diff --git a/src/slic3r/GUI/MediaPlayCtrl.h b/src/slic3r/GUI/MediaPlayCtrl.h index 0a01daefc9..e8ad0ee82c 100644 --- a/src/slic3r/GUI/MediaPlayCtrl.h +++ b/src/slic3r/GUI/MediaPlayCtrl.h @@ -9,6 +9,8 @@ #define MediaPlayCtrl_h #include "wxMediaCtrl3.h" +#include "IMediaController.hpp" +#include "slic3r/Utils/IPrinterAgent.hpp" #include @@ -36,6 +38,10 @@ public: void SetMachineObject(MachineObject * obj); + void SetWebMediaController(IMediaController *ctrl); + + void StopWebStream(); + bool IsStreaming() const; void ToggleStream(); @@ -66,6 +72,8 @@ private: static bool get_stream_url(std::string *url = nullptr); + CameraStreamMode current_mode() const; + private: static inline const wxMediaState MEDIASTATE_IDLE = static_cast(3); static inline const wxMediaState MEDIASTATE_INITIALIZING = static_cast(4); @@ -76,6 +84,9 @@ private: std::shared_ptr m_token = std::make_shared(0); wxMediaCtrl3 * m_media_ctrl; + IMediaController * m_web_ctrl = nullptr; + std::string m_agent_camera_url; + bool m_web_user_stopped = false; wxMediaState m_last_state = MEDIASTATE_IDLE; std::string m_machine; int m_lan_proto = 0; diff --git a/src/slic3r/GUI/StatusPanel.cpp b/src/slic3r/GUI/StatusPanel.cpp index 97cd1c6e2e..b00dee3dca 100644 --- a/src/slic3r/GUI/StatusPanel.cpp +++ b/src/slic3r/GUI/StatusPanel.cpp @@ -1494,27 +1494,26 @@ wxBoxSizer *StatusBasePanel::create_monitoring_page() m_setting_button->SetMinSize(wxSize(FromDIP(38), FromDIP(24))); m_setting_button->SetBackgroundColour(STATUS_TITLE_BG); - m_camera_switch_button = new wxStaticBitmap(m_panel_monitoring_title, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize(FromDIP(38), FromDIP(24)), 0); - m_camera_switch_button->SetMinSize(wxSize(FromDIP(38), FromDIP(24))); - m_camera_switch_button->SetBackgroundColour(STATUS_TITLE_BG); - m_camera_switch_button->SetBitmap(m_bitmap_switch_camera.bmp()); - m_camera_switch_button->Bind(wxEVT_LEFT_DOWN, &StatusBasePanel::on_camera_switch_toggled, this); - m_camera_switch_button->Bind(wxEVT_RIGHT_DOWN, [this](auto& e) { - const std::string js_request_pip = R"( - document.querySelector('video').requestPictureInPicture(); - )"; - m_custom_camera_view->RunScript(js_request_pip); - }); - m_camera_switch_button->Hide(); + // m_camera_switch_button = new wxStaticBitmap(m_panel_monitoring_title, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize(FromDIP(38), FromDIP(24)), 0); + // m_camera_switch_button->SetMinSize(wxSize(FromDIP(38), FromDIP(24))); + // m_camera_switch_button->SetBackgroundColour(STATUS_TITLE_BG); + // m_camera_switch_button->SetBitmap(m_bitmap_switch_camera.bmp()); + // m_camera_switch_button->Bind(wxEVT_RIGHT_DOWN, [this](auto& e) { + // const std::string js_request_pip = R"( + // document.querySelector('video').requestPictureInPicture(); + // )"; + // m_custom_camera_view->RunScript(js_request_pip); + // }); + // m_camera_switch_button->Hide(); m_bitmap_sdcard_img->SetToolTip(_L("Storage")); m_bitmap_timelapse_img->SetToolTip(_L("Timelapse")); m_bitmap_recording_img->SetToolTip(_L("Video")); m_bitmap_vcamera_img->SetToolTip(_L("Go Live")); m_setting_button->SetToolTip(_L("Camera Setting")); - m_camera_switch_button->SetToolTip(_L("Switch Camera View")); + // m_camera_switch_button->SetToolTip(_L("Switch Camera View")); - bSizer_monitoring_title->Add(m_camera_switch_button, 0, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5)); + // bSizer_monitoring_title->Add(m_camera_switch_button, 0, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5)); bSizer_monitoring_title->Add(m_bitmap_sdcard_img, 0, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5)); bSizer_monitoring_title->Add(m_bitmap_timelapse_img, 0, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5)); bSizer_monitoring_title->Add(m_bitmap_recording_img, 0, wxALIGN_CENTER_VERTICAL | wxALL, FromDIP(5)); @@ -1537,19 +1536,18 @@ wxBoxSizer *StatusBasePanel::create_monitoring_page() m_custom_camera_view = WebView::CreateWebView(this, wxEmptyString); m_custom_camera_view->EnableContextMenu(false); Bind(wxEVT_WEBVIEW_NAVIGATING, &StatusBasePanel::on_webview_navigating, this, m_custom_camera_view->GetId()); + m_web_media_controller = std::make_unique(m_custom_camera_view); m_media_play_ctrl = new MediaPlayCtrl(this, m_media_ctrl, wxDefaultPosition, wxSize(-1, FromDIP(40))); + m_media_play_ctrl->SetWebMediaController(m_web_media_controller.get()); m_custom_camera_view->Hide(); - m_custom_camera_view->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, [this](wxWebViewEvent& evt) { - if (evt.GetString() == "leavepictureinpicture") { - // When leaving PiP, video gets paused in some cases and toggling play - // programmatically does not work. - m_custom_camera_view->Reload(); - } - else if (evt.GetString() == "enterpictureinpicture") { - toggle_builtin_camera(); - } - }); + // m_custom_camera_view->Bind(wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, [this](wxWebViewEvent& evt) { + // if (evt.GetString() == "leavepictureinpicture") { + // // When leaving PiP, video gets paused in some cases and toggling play + // // programmatically does not work. + // m_custom_camera_view->Reload(); + // } + // }); sizer->Add(m_media_ctrl, 1, wxEXPAND | wxALL, 0); sizer->Add(m_custom_camera_view, 1, wxEXPAND | wxALL, 0); @@ -1559,10 +1557,6 @@ wxBoxSizer *StatusBasePanel::create_monitoring_page() // // sizer->Add(media_ctrl_panel, 1, wxEXPAND | wxALL, 1); - if (wxGetApp().app_config->get("camera", "enable_custom_source") == "true") { - handle_camera_source_change(); - } - return sizer; } @@ -2312,25 +2306,21 @@ void StatusPanel::update_camera_state(MachineObject* obj) { if (!obj) return; - const bool has_printer_webcam = !obj->webcam_stream_url.empty(); + auto agent = wxGetApp().getAgent(); + const auto camera_mode = agent ? agent->get_camera_stream_mode() : CameraStreamMode::none; + const bool has_printer_webcam = camera_mode == CameraStreamMode::http || camera_mode == CameraStreamMode::http_snapshot; if (has_printer_webcam) { - if (m_printer_webcam_url != obj->webcam_stream_url) { - m_custom_camera_view->LoadURL(obj->webcam_stream_url); - m_custom_camera_view->Show(); - m_media_ctrl->Hide(); - m_media_play_ctrl->Hide(); - m_printer_webcam_url = obj->webcam_stream_url; - } - m_camera_switch_button->Hide(); + //m_camera_switch_button->Hide(); if (!m_custom_camera_view->IsShown()) { - // why: do not compare or reload the WebView URL per tick, or redirects can cause a reload loop. + // why: do not reload the WebView URL per tick, or redirects can cause a reload loop. + // MediaPlayCtrl (via its WebMediaController) owns loading/playing the stream itself. m_custom_camera_view->Show(); m_media_ctrl->Hide(); - m_media_play_ctrl->Hide(); } - } else if (!m_printer_webcam_url.empty()) { - handle_camera_source_change(); - m_printer_webcam_url.clear(); + } else if (m_custom_camera_view->IsShown()) { + m_custom_camera_view->Hide(); + m_media_ctrl->Show(); + m_media_play_ctrl->StopWebStream(); } //sdcard @@ -5056,7 +5046,6 @@ void StatusPanel::on_camera_enter(wxMouseEvent& event) } sdcard_hint_dlg->on_show(); }); - m_camera_popup->Bind(EVT_CAM_SOURCE_CHANGE, &StatusPanel::on_camera_source_change, this); wxWindow* ctrl = (wxWindow*)event.GetEventObject(); wxPoint pos = ctrl->ClientToScreen(wxPoint(0, 0)); wxSize sz = ctrl->GetSize(); @@ -5068,54 +5057,6 @@ void StatusPanel::on_camera_enter(wxMouseEvent& event) } } -void StatusBasePanel::on_camera_source_change(wxCommandEvent& event) -{ - handle_camera_source_change(); -} - -void StatusBasePanel::handle_camera_source_change() -{ - const auto new_cam_url = wxGetApp().app_config->get("camera", "custom_source"); - const auto enabled = wxGetApp().app_config->get("camera", "enable_custom_source") == "true"; - - if (enabled && !new_cam_url.empty()) { - m_custom_camera_view->LoadURL(new_cam_url); - toggle_custom_camera(); - m_camera_switch_button->Show(); - } else { - toggle_builtin_camera(); - m_camera_switch_button->Hide(); - } -} - -void StatusBasePanel::toggle_builtin_camera() -{ - m_custom_camera_view->Hide(); - m_media_ctrl->Show(); - m_media_play_ctrl->Show(); -} - -void StatusBasePanel::toggle_custom_camera() -{ - const auto enabled = wxGetApp().app_config->get("camera", "enable_custom_source") == "true"; - - if (enabled) { - m_custom_camera_view->Show(); - m_media_ctrl->Hide(); - m_media_play_ctrl->Hide(); - } -} - -void StatusBasePanel::on_camera_switch_toggled(wxMouseEvent& event) -{ - const auto enabled = wxGetApp().app_config->get("camera", "enable_custom_source") == "true"; - if (enabled && m_media_ctrl->IsShown()) { - toggle_custom_camera(); - } else { - toggle_builtin_camera(); - } -} - void StatusBasePanel::remove_controls() { const std::string js_cleanup_video_element = R"( @@ -5268,9 +5209,10 @@ bool StatusPanel::is_stage_list_info_changed(MachineObject *obj) void StatusPanel::set_default() { BOOST_LOG_TRIVIAL(trace) << "status_panel: set_default"; - if (!m_printer_webcam_url.empty()) { - handle_camera_source_change(); - m_printer_webcam_url.clear(); + if (m_custom_camera_view->IsShown()) { + m_custom_camera_view->Hide(); + m_media_ctrl->Show(); + m_media_play_ctrl->StopWebStream(); } obj = nullptr; last_subtask = nullptr; diff --git a/src/slic3r/GUI/StatusPanel.hpp b/src/slic3r/GUI/StatusPanel.hpp index ec863cb474..1478feef8b 100644 --- a/src/slic3r/GUI/StatusPanel.hpp +++ b/src/slic3r/GUI/StatusPanel.hpp @@ -15,7 +15,9 @@ #include #include #include +#include #include "MediaPlayCtrl.h" +#include "WebMediaController.hpp" #include "AMSSetting.hpp" #include "Calibration.hpp" #include "CalibrationWizardPage.hpp" @@ -440,7 +442,7 @@ protected: wxStaticBitmap *m_bitmap_sdcard_img; wxStaticBitmap *m_bitmap_static_use_time; wxStaticBitmap *m_bitmap_static_use_weight; - wxStaticBitmap* m_camera_switch_button; + // wxStaticBitmap* m_camera_switch_button; wxMediaCtrl3 * m_media_ctrl; @@ -462,6 +464,8 @@ protected: ScalableButton *m_button_abort; Button * m_button_clean; wxWebView * m_custom_camera_view{nullptr}; + std::unique_ptr m_web_media_controller; + wxSimplebook* m_extruder_book; std::vector m_extruderImage; @@ -577,13 +581,8 @@ protected: virtual void on_axis_ctrl_e_up_10(wxCommandEvent &event) { event.Skip(); } virtual void on_axis_ctrl_e_down_10(wxCommandEvent &event) { event.Skip(); } virtual void on_nozzle_selected(wxCommandEvent &event) { event.Skip(); } - void on_camera_source_change(wxCommandEvent& event); - void handle_camera_source_change(); void remove_controls(); void on_webview_navigating(wxWebViewEvent& evt); - void on_camera_switch_toggled(wxMouseEvent& event); - void toggle_custom_camera(); - void toggle_builtin_camera(); public: StatusBasePanel(wxWindow * parent, @@ -664,7 +663,6 @@ protected: int m_last_timelapse = -1; int m_last_extrusion = -1; int m_last_vcamera = -1; - std::string m_printer_webcam_url; int m_model_mall_request_count = 0; bool m_is_load_with_temp = false; json m_rating_result; diff --git a/src/slic3r/GUI/WebMediaController.cpp b/src/slic3r/GUI/WebMediaController.cpp new file mode 100644 index 0000000000..13d820c98f --- /dev/null +++ b/src/slic3r/GUI/WebMediaController.cpp @@ -0,0 +1,83 @@ +#include "WebMediaController.hpp" + +#include + +namespace Slic3r { namespace GUI { + +WebMediaController::WebMediaController(wxWebView *webview) : m_webview(webview) +{ + if (!m_webview) + return; + + m_webview->SetBackgroundColour(*wxBLACK); + m_webview->SetPage( + "", + ""); +} + +void WebMediaController::Load(wxURI url) +{ + Load(url, CameraStreamMode::http); +} + +void WebMediaController::Load(wxURI url, CameraStreamMode mode) +{ + m_url = url.BuildURI().ToStdString(); + m_stream_mode = mode; +} + +void WebMediaController::Play() +{ + if (!m_webview) + return; + + wxString url = wxString::FromUTF8(m_url); + wxString html = ""; + } else { + html += " src=\"" + url + "\">"; + } + m_webview->SetPage(html, url); +} + +void WebMediaController::Stop() +{ + m_url.clear(); + if (m_webview) + m_webview->Stop(); +} + +// wxMediaState WebMediaController::GetState() +// { +// return wxMediaState{}; +// } + +// int WebMediaController::GetLastError() const +// { +// return 0; +// } + +// wxSize WebMediaController::GetVideoSize() const +// { +// return wxSize{}; +// } + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/WebMediaController.hpp b/src/slic3r/GUI/WebMediaController.hpp new file mode 100644 index 0000000000..cdbee33cc3 --- /dev/null +++ b/src/slic3r/GUI/WebMediaController.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include + +class wxWebView; + +namespace Slic3r { namespace GUI { + +class WebMediaController : public IMediaController +{ +public: + explicit WebMediaController(wxWebView *webview); + + void Load(wxURI url) override; + + void Load(wxURI url, CameraStreamMode mode) override; + + void Play() override; + + void Stop() override; + + // wxMediaState GetState() override; + + // int GetLastError() const override; + + // wxSize GetVideoSize() const override; + +private: + wxWebView * m_webview; + std::string m_url; + CameraStreamMode m_stream_mode = CameraStreamMode::http; +}; + +}} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/wxMediaCtrl3.cpp b/src/slic3r/GUI/wxMediaCtrl3.cpp index 098db68808..d0d53072d6 100644 --- a/src/slic3r/GUI/wxMediaCtrl3.cpp +++ b/src/slic3r/GUI/wxMediaCtrl3.cpp @@ -4,6 +4,9 @@ #include "libslic3r/Utils.hpp" #include #include +extern "C" { +#include +} #ifdef __WIN32__ #include #include @@ -185,6 +188,106 @@ void wxMediaCtrl3::bambu_log(void *ctx, int level, tchar const *msg2) BOOST_LOG_TRIVIAL(info) << msg.ToUTF8().data(); } +int wxMediaCtrl3::rtsp_interrupt_callback(void *opaque) +{ + auto *ctrl = static_cast(opaque); + std::lock_guard lock(ctrl->m_mutex); + return ctrl->m_url != ctrl->m_active_url; +} + +int wxMediaCtrl3::PlayRtsp(std::shared_ptr const &url, std::unique_lock &lock) +{ + if (avformat_network_init() < 0) + return 2; + + AVFormatContext *format_context = avformat_alloc_context(); + if (!format_context) { + avformat_network_deinit(); + return 2; + } + + format_context->interrupt_callback = {&wxMediaCtrl3::rtsp_interrupt_callback, this}; + m_active_url = url; + + auto finish = [&](int error) { + lock.unlock(); + avformat_close_input(&format_context); + avformat_network_deinit(); + lock.lock(); + m_active_url.reset(); + return error; + }; + + const std::string uri = url->BuildURI().ToUTF8().data(); + AVDictionary *options = nullptr; + av_dict_set(&options, "rtsp_transport", "tcp", 0); + lock.unlock(); + int error = avformat_open_input(&format_context, uri.c_str(), nullptr, &options); + av_dict_free(&options); + lock.lock(); + if (error < 0) + return finish(2); + + lock.unlock(); + error = avformat_find_stream_info(format_context, nullptr); + lock.lock(); + if (error < 0) + return finish(2); + + const int video_stream = av_find_best_stream(format_context, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0); + if (video_stream < 0) + return finish(2); + + AVVideoDecoder decoder; + 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(); + + AVPacket *packet = av_packet_alloc(); + if (!packet) + return finish(2); + + while (m_url == url) { + lock.unlock(); + error = av_read_frame(format_context, packet); + lock.lock(); + if (m_url != url) + break; + if (error < 0) + break; + if (packet->stream_index == video_stream) { + const int decode_error = decoder.decode(*packet); + if (decode_error == 0) { + auto frame_size = m_frame_size; + lock.unlock(); +#ifdef _WIN32 + wxBitmap frame; + decoder.toWxBitmap(frame, frame_size); +#else + wxImage frame; + decoder.toWxImage(frame, frame_size); +#endif + lock.lock(); + if (m_url != url) + break; + if (frame.IsOk()) + m_frame = frame; + CallAfter([this] { Refresh(); }); + } + } + av_packet_unref(packet); + } + + av_packet_free(&packet); + return finish(m_url == url ? 2 : 1); +} + void wxMediaCtrl3::PlayThread() { using namespace std::chrono_literals; @@ -197,42 +300,21 @@ void wxMediaCtrl3::PlayThread() continue; if (!url->HasScheme()) break; - lk.unlock(); - Bambu_Tunnel tunnel = nullptr; - int error = Bambu_Create(&tunnel, m_url->BuildURI().ToUTF8()); - if (error == 0) { - Bambu_SetLogger(tunnel, &wxMediaCtrl3::bambu_log, this); - error = Bambu_Open(tunnel); - if (error == 0) - error = Bambu_would_block; - } - lk.lock(); - while (error == int(Bambu_would_block)) { - m_cond.wait_for(lk, 100ms); - if (m_url != url) { - error = 1; - break; + const wxString scheme = url->GetScheme(); + const bool generic_rtsp = scheme.CmpNoCase("rtsp") == 0 || scheme.CmpNoCase("rtsps") == 0; + int error = 0; + if (generic_rtsp) { + error = PlayRtsp(url, lk); + } else { + lk.unlock(); + Bambu_Tunnel tunnel = nullptr; + error = Bambu_Create(&tunnel, m_url->BuildURI().ToUTF8()); + if (error == 0) { + Bambu_SetLogger(tunnel, &wxMediaCtrl3::bambu_log, this); + error = Bambu_Open(tunnel); + if (error == 0) + error = Bambu_would_block; } - lk.unlock(); - error = Bambu_StartStream(tunnel, true); - lk.lock(); - } - Bambu_StreamInfo info; - if (error == 0) - error = Bambu_GetStreamInfo(tunnel, 0, &info); - AVVideoDecoder decoder; - int minFrameDuration = 0; - if (error == 0) { - decoder.open(info); - m_video_size = { info.format.video.width, info.format.video.height }; - adjust_frame_size(m_frame_size, m_video_size, GetSize()); - minFrameDuration = 800 / info.format.video.frame_rate; // 80% - NotifyStopped(); - } - Bambu_Sample sample; - while (error == 0) { - lk.unlock(); - error = Bambu_ReadSample(tunnel, &sample); lk.lock(); while (error == int(Bambu_would_block)) { m_cond.wait_for(lk, 100ms); @@ -240,59 +322,87 @@ void wxMediaCtrl3::PlayThread() error = 1; break; } + lk.unlock(); + error = Bambu_StartStream(tunnel, true); + lk.lock(); + } + Bambu_StreamInfo info; + if (error == 0) + error = Bambu_GetStreamInfo(tunnel, 0, &info); + AVVideoDecoder decoder; + int minFrameDuration = 0; + if (error == 0) { + decoder.open(info); + m_video_size = { info.format.video.width, info.format.video.height }; + adjust_frame_size(m_frame_size, m_video_size, GetSize()); + minFrameDuration = 800 / info.format.video.frame_rate; // 80% + NotifyStopped(); + } + Bambu_Sample sample; + while (error == 0) { lk.unlock(); error = Bambu_ReadSample(tunnel, &sample); lk.lock(); - } - if (error == 0) { - auto frame_size = m_frame_size; - lk.unlock(); - decoder.decode(sample); -#ifdef _WIN32 - wxBitmap bm; - decoder.toWxBitmap(bm, frame_size); -#else - wxImage bm; - decoder.toWxImage(bm, frame_size); -#endif - lk.lock(); - if (m_url != url) { - error = 1; - break; - } - if (bm.IsOk()) { - auto now = std::chrono::system_clock::now(); - if (m_last_PTS && (sample.decode_time - m_last_PTS) < 30000000ULL) { // 3s - auto next_PTS_expected = m_last_PTS_expected + std::chrono::milliseconds((sample.decode_time - m_last_PTS) / 10000ULL); - // The frame is late, catch up a little - auto next_PTS_practical = m_last_PTS_practical + std::chrono::milliseconds(minFrameDuration); - auto next_PTS = std::max(next_PTS_expected, next_PTS_practical); - if(now < next_PTS) - std::this_thread::sleep_until(next_PTS); - else - next_PTS = now; - //auto text = wxString::Format(L"wxMediaCtrl3 pts diff %ld\n", std::chrono::duration_cast(next_PTS - next_PTS_expected).count()); - //OutputDebugString(text); - m_last_PTS = sample.decode_time; - m_last_PTS_expected = next_PTS_expected; - m_last_PTS_practical = next_PTS; - } else { - // Resync - m_last_PTS = sample.decode_time; - m_last_PTS_expected = now; - m_last_PTS_practical = now; + while (error == int(Bambu_would_block)) { + m_cond.wait_for(lk, 100ms); + if (m_url != url) { + error = 1; + break; } - m_frame = bm; + lk.unlock(); + error = Bambu_ReadSample(tunnel, &sample); + lk.lock(); + } + if (error == 0) { + auto frame_size = m_frame_size; + lk.unlock(); + decoder.decode(sample); +#ifdef _WIN32 + wxBitmap bm; + decoder.toWxBitmap(bm, frame_size); +#else + wxImage bm; + decoder.toWxImage(bm, frame_size); +#endif + lk.lock(); + if (m_url != url) { + error = 1; + break; + } + if (bm.IsOk()) { + auto now = std::chrono::system_clock::now(); + if (m_last_PTS && (sample.decode_time - m_last_PTS) < 30000000ULL) { // 3s + auto next_PTS_expected = m_last_PTS_expected + std::chrono::milliseconds((sample.decode_time - m_last_PTS) / 10000ULL); + // The frame is late, catch up a little + auto next_PTS_practical = m_last_PTS_practical + std::chrono::milliseconds(minFrameDuration); + auto next_PTS = std::max(next_PTS_expected, next_PTS_practical); + if(now < next_PTS) + std::this_thread::sleep_until(next_PTS); + else + next_PTS = now; + //auto text = wxString::Format(L"wxMediaCtrl3 pts diff %ld\n", std::chrono::duration_cast(next_PTS - next_PTS_expected).count()); + //OutputDebugString(text); + m_last_PTS = sample.decode_time; + m_last_PTS_expected = next_PTS_expected; + m_last_PTS_practical = next_PTS; + } else { + // Resync + m_last_PTS = sample.decode_time; + m_last_PTS_expected = now; + m_last_PTS_practical = now; + } + m_frame = bm; + } + CallAfter([this] { Refresh(); }); } - CallAfter([this] { Refresh(); }); } - } - if (tunnel) { - lk.unlock(); - Bambu_Close(tunnel); - Bambu_Destroy(tunnel); - tunnel = nullptr; - lk.lock(); + if (tunnel) { + lk.unlock(); + Bambu_Close(tunnel); + Bambu_Destroy(tunnel); + tunnel = nullptr; + lk.lock(); + } } if (m_url == url) m_error = error; diff --git a/src/slic3r/GUI/wxMediaCtrl3.h b/src/slic3r/GUI/wxMediaCtrl3.h index 1d64955ffd..bcc94a17ef 100644 --- a/src/slic3r/GUI/wxMediaCtrl3.h +++ b/src/slic3r/GUI/wxMediaCtrl3.h @@ -56,8 +56,10 @@ protected: void DoSetSize(int x, int y, int width, int height, int sizeFlags) override; static void bambu_log(void *ctx, int level, tchar const *msg); + static int rtsp_interrupt_callback(void *opaque); void PlayThread(); + int PlayRtsp(std::shared_ptr const &url, std::unique_lock &lock); void NotifyStopped(); @@ -74,6 +76,7 @@ private: #endif std::shared_ptr m_url; + std::shared_ptr m_active_url; std::uint64_t m_last_PTS{0}; std::chrono::system_clock::time_point m_last_PTS_expected; std::chrono::system_clock::time_point m_last_PTS_practical; diff --git a/src/slic3r/Utils/IPrinterAgent.hpp b/src/slic3r/Utils/IPrinterAgent.hpp index b77a27276f..c076b57be5 100644 --- a/src/slic3r/Utils/IPrinterAgent.hpp +++ b/src/slic3r/Utils/IPrinterAgent.hpp @@ -81,6 +81,14 @@ enum class FilamentSyncMode { pull ///< On-demand fetch via REST API (blocking call) }; +enum class CameraStreamMode { + none = 0, + http, // LAN or Cloud + rtsp, // LAN only + webrtc, // Cloud only + http_snapshot // HTTP endpoint returning one image per request +}; + /** * IPrinterAgent - Interface for printer operations. * @@ -354,12 +362,26 @@ public: */ virtual FilamentSyncMode get_filament_sync_mode() const { return FilamentSyncMode::none; } + /** + * Get the camera stream mode for this agent. This value can be deterministic and derived at + * runtime if the printer supports multiple camera stream modes. E.g. LAN => HTTP/RTSP, Cloud => WebRTC. + * + * @return CameraStreamMode indicating how the camera stream is obtained or used: + */ + virtual CameraStreamMode get_camera_stream_mode() const { return CameraStreamMode::none; } + /** * Refresh filament info from the printer synchronously. * Should only be called when get_filament_sync_mode() returns FilamentSyncMode::pull. * Populates the MachineObject's DevFilaSystem with fetched filament data. */ virtual bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) { return false; } + + /** + * Get the current camera stream URL for this agent's active machine. + * Only meaningful when get_camera_stream_mode() returns an HTTP or RTSP mode. + */ + virtual std::string get_camera_url() const { return {}; } }; } // namespace Slic3r diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp index d898c51e3a..d2c8781fbe 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.cpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.cpp @@ -750,6 +750,57 @@ bool MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSync return false; } +CameraStreamMode MoonrakerPrinterAgent::get_camera_stream_mode() const +{ + refresh_webcam_info(); + std::lock_guard lock(payload_mutex); + return webcam_stream_mode; +} + +std::string MoonrakerPrinterAgent::get_camera_url() const +{ + refresh_webcam_info(); + std::lock_guard lock(payload_mutex); + return webcam_stream_url; +} + +void MoonrakerPrinterAgent::refresh_webcam_info() const +{ + std::string base_url; + std::string api_key; + uint64_t generation; + { + std::lock_guard lock(connect_mutex); + base_url = device_info.base_url; + api_key = device_info.api_key; + generation = connect_generation.load(); + } + + const uint64_t now_ms = static_cast( + std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count()); + + if (base_url.empty()) { + std::lock_guard lock(payload_mutex); + webcam_stream_url.clear(); + webcam_stream_mode = CameraStreamMode::none; + webcam_info_last_lookup_ms = 0; + webcam_info_generation = generation; + return; + } + + { + std::lock_guard lock(payload_mutex); + if (webcam_info_generation == generation && + now_ms - webcam_info_last_lookup_ms < WEBCAM_INFO_REFRESH_INTERVAL_MS) { + return; + } + webcam_info_generation = generation; + webcam_info_last_lookup_ms = now_ms; + } + + fetch_webcam_info(base_url, api_key, generation); +} + std::string MoonrakerPrinterAgent::trim_and_upper(const std::string& input) { std::string result = input; @@ -1605,10 +1656,11 @@ bool MoonrakerPrinterAgent::send_ws_rpc(const std::string& method, const nlohman return false; } -bool MoonrakerPrinterAgent::fetch_webcam_info(const std::string& base_url, const std::string& api_key, uint64_t generation) +bool MoonrakerPrinterAgent::fetch_webcam_info(const std::string& base_url, const std::string& api_key, uint64_t generation) const { - std::string stream_url; + std::string camera_url; std::string webcam_name; + CameraStreamMode stream_mode = CameraStreamMode::none; std::string error; try { std::string response_body; @@ -1649,16 +1701,24 @@ bool MoonrakerPrinterAgent::fetch_webcam_info(const std::string& base_url, const error = "Unexpected JSON structure"; } else { for (const auto& webcam : result["webcams"]) { - if (webcam.is_object() && webcam.value("enabled", false) && webcam.contains("stream_url") && - webcam["stream_url"].is_string()) { - stream_url = webcam["stream_url"].get(); + if (webcam.is_object() && webcam.value("enabled", false)) { + if (webcam.contains("stream_url") && webcam["stream_url"].is_string() && + !webcam["stream_url"].get().empty()) { + camera_url = webcam["stream_url"].get(); + stream_mode = CameraStreamMode::http; + } else if (webcam.contains("snapshot_url") && webcam["snapshot_url"].is_string() && + !webcam["snapshot_url"].get().empty()) { + camera_url = webcam["snapshot_url"].get(); + stream_mode = CameraStreamMode::http_snapshot; + } if (webcam.contains("name") && webcam["name"].is_string()) { webcam_name = webcam["name"].get(); } - break; + if (!camera_url.empty()) + break; } } - if (stream_url.empty()) { + if (camera_url.empty()) { error = "No enabled webcam"; } } @@ -1666,8 +1726,8 @@ bool MoonrakerPrinterAgent::fetch_webcam_info(const std::string& base_url, const } if (error.empty()) { - if (stream_url.rfind("http", 0) != 0 && !stream_url.empty() && stream_url.front() == '/') { - // why: Moonraker's API port serves a JSON 404 for /webcam; relative streams use the printer web root. + if (camera_url.rfind("http", 0) != 0 && !camera_url.empty() && camera_url.front() == '/') { + // why: Moonraker's API port serves a JSON 404 for /webcam; relative camera URLs use the printer web root. const size_t scheme_end = base_url.find("://"); const size_t authority_start = scheme_end == std::string::npos ? 0 : scheme_end + 3; const size_t authority_end = base_url.find('/', authority_start); @@ -1678,9 +1738,9 @@ bool MoonrakerPrinterAgent::fetch_webcam_info(const std::string& base_url, const std::all_of(authority.begin() + port_start + 1, authority.end(), [](char c) { return c >= '0' && c <= '9'; })) { authority.erase(port_start); } - stream_url = scheme + authority + stream_url; - } else if (stream_url.rfind("http", 0) != 0) { - error = "Unsupported webcam stream URL"; + camera_url = scheme + authority + camera_url; + } else if (camera_url.rfind("http", 0) != 0) { + error = "Unsupported webcam URL"; } } } catch (const std::exception& e) { @@ -1692,14 +1752,15 @@ bool MoonrakerPrinterAgent::fetch_webcam_info(const std::string& base_url, const { std::lock_guard lock(payload_mutex); if (generation == connect_generation.load()) { - webcam_stream_url = error.empty() ? stream_url : ""; + webcam_stream_url = error.empty() ? camera_url : ""; + webcam_stream_mode = error.empty() ? stream_mode : CameraStreamMode::none; } } if (!error.empty()) { BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: webcam discovery failed: " << error; return false; } - BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: selected webcam '" << webcam_name << "' with stream URL " << stream_url; + BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: selected webcam '" << webcam_name << "' with camera URL " << camera_url; return true; } @@ -2880,8 +2941,6 @@ void MoonrakerPrinterAgent::perform_connection_async(const std::string& dev_id, BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: Initial status query failed: " << error_msg; } - fetch_webcam_info(base_url, api_key, generation); - // Start WebSocket status stream start_status_stream(dev_id, base_url, api_key); #endif diff --git a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp index 19ef02ee22..8750085181 100644 --- a/src/slic3r/Utils/MoonrakerPrinterAgent.hpp +++ b/src/slic3r/Utils/MoonrakerPrinterAgent.hpp @@ -73,10 +73,9 @@ public: int set_on_local_connect_fn(OnLocalConnectedFn fn) override; int set_on_local_message_fn(OnMessageFn fn) override; int set_queue_on_main_fn(QueueOnMainFn fn) override; - - // Pull-mode agent (on-demand filament sync) - FilamentSyncMode get_filament_sync_mode() const override { return FilamentSyncMode::pull; } bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override; + CameraStreamMode get_camera_stream_mode() const override; + std::string get_camera_url() const override; protected: struct MoonrakerDeviceInfo @@ -153,7 +152,6 @@ private: bool fetch_object_list(const std::string& base_url, const std::string& api_key, std::set& objects, std::string& error) const; bool query_printer_status(const std::string& base_url, const std::string& api_key, nlohmann::json& status, std::string& error) const; - bool fetch_webcam_info(const std::string& base_url, const std::string& api_key, uint64_t generation); void announce_printhost_device(); void dispatch_local_connect(int state, const std::string& dev_id, const std::string& msg); @@ -188,6 +186,12 @@ private: const std::string& api_key, uint64_t generation); + // why: a printer with no /server/webcams/list entry can still name its stream directly; + // subclasses (e.g. printers with a fixed webcam path) can override this instead. + virtual std::string webcam_stream_override(const std::string& base_url) const { return {}; } + void refresh_webcam_info() const; + bool fetch_webcam_info(const std::string& base_url, const std::string& api_key, uint64_t generation) const; + // System-specific filament fetch methods bool fetch_hh_filament_info(std::vector& trays, int& max_lane_index); bool fetch_moonraker_filament_data(std::vector& trays, int& max_lane_index); @@ -219,9 +223,14 @@ private: // note: guarded by payload_mutex; filled by refresh_thumbnail_url(), empty url = looked up, none found std::string thumbnail_filename; std::string thumbnail_url; - std::string webcam_stream_url; + mutable std::string webcam_stream_url; + mutable CameraStreamMode webcam_stream_mode = CameraStreamMode::none; + mutable uint64_t webcam_info_last_lookup_ms = 0; + mutable uint64_t webcam_info_generation = 0; unsigned thumbnail_lookup_attempts = 0; + static constexpr uint64_t WEBCAM_INFO_REFRESH_INTERVAL_MS = 1000; + std::atomic next_jsonrpc_id{1}; std::set available_objects; // Track for feature detection bool assumed_light_on = false; diff --git a/src/slic3r/Utils/NetworkAgent.cpp b/src/slic3r/Utils/NetworkAgent.cpp index eca59d30ce..01ed03f84f 100644 --- a/src/slic3r/Utils/NetworkAgent.cpp +++ b/src/slic3r/Utils/NetworkAgent.cpp @@ -1026,6 +1026,20 @@ bool NetworkAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode sync return false; } +CameraStreamMode NetworkAgent::get_camera_stream_mode() const +{ + if (m_printer_agent) + return m_printer_agent->get_camera_stream_mode(); + return CameraStreamMode::none; +} + +std::string NetworkAgent::get_local_camera_stream_url() const +{ + if (m_printer_agent) + return m_printer_agent->get_camera_url(); + return {}; +} + int NetworkAgent::request_bind_ticket(std::string* ticket) { if (m_printer_agent) diff --git a/src/slic3r/Utils/NetworkAgent.hpp b/src/slic3r/Utils/NetworkAgent.hpp index e9d05143a1..42ded7e885 100644 --- a/src/slic3r/Utils/NetworkAgent.hpp +++ b/src/slic3r/Utils/NetworkAgent.hpp @@ -181,6 +181,8 @@ public: int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn); FilamentSyncMode get_filament_sync_mode() const; bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull); + CameraStreamMode get_camera_stream_mode() const; + std::string get_local_camera_stream_url() const; int request_bind_ticket(std::string* ticket); int get_hms_snapshot(std::string dev_id, std::string file_name, std::function callback); diff --git a/src/slic3r/Utils/SnapmakerPrinterAgent.cpp b/src/slic3r/Utils/SnapmakerPrinterAgent.cpp index fe1a29af53..cd2241e0d6 100644 --- a/src/slic3r/Utils/SnapmakerPrinterAgent.cpp +++ b/src/slic3r/Utils/SnapmakerPrinterAgent.cpp @@ -96,15 +96,6 @@ void SnapmakerPrinterAgent::on_status_loop_tick(const std::string& dev_id) } } -int SnapmakerPrinterAgent::connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) -{ - const int rtn = MoonrakerPrinterAgent::connect_printer(dev_id, dev_ip, username, password, use_ssl); - if (rtn == BAMBU_NETWORK_SUCCESS) { - start_camera_monitor(); - } - return rtn; -} - int SnapmakerPrinterAgent::command_start_camera(std::string dev_id) { (void) dev_id; @@ -149,127 +140,139 @@ std::string SnapmakerPrinterAgent::combine_filament_type(const std::string& type bool SnapmakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode) { + (void) dev_id; if (sync_mode != get_filament_sync_mode()) return false; - std::string url = join_url(device_info.base_url, "/printer/objects/query?print_task_config&filament_detect"); + const std::string base_url = device_info.base_url; + const std::string api_key = device_info.api_key; - std::string response_body; - bool success = false; - std::string http_error; + filament_fetch_in_flight.fetch_add(1, std::memory_order_relaxed); - auto http = Http::get(url); - if (!device_info.api_key.empty()) { - http.header("X-Api-Key", device_info.api_key); - } - http.timeout_connect(5) - .timeout_max(10) - .on_complete([&](std::string body, unsigned status) { - if (status == 200) { - response_body = body; - success = true; - } else { - http_error = "HTTP error: " + std::to_string(status); - } - }) - .on_error([&](std::string body, std::string err, unsigned status) { - http_error = err; - if (status > 0) { - http_error += " (HTTP " + std::to_string(status) + ")"; - } - }) - .perform_sync(); + std::thread([this, base_url, api_key]() { + struct InFlightGuard + { + std::atomic& counter; + ~InFlightGuard() { counter.fetch_sub(1, std::memory_order_relaxed); } + } guard{filament_fetch_in_flight}; - if (!success) { - BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: HTTP request failed: " << http_error; - return false; - } + const std::string url = join_url(base_url, "/printer/objects/query?print_task_config&filament_detect"); - auto json = nlohmann::json::parse(response_body, nullptr, false, true); - if (json.is_discarded()) { - BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: Invalid JSON response"; - return false; - } + std::string response_body; + bool success = false; + std::string http_error; - // Navigate to result.status.print_task_config - if (!json.contains("result") || !json["result"].contains("status") || - !json["result"]["status"].contains("print_task_config")) { - BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: Missing print_task_config in response"; - return false; - } - - auto& ptc = json["result"]["status"]["print_task_config"]; - - // Read parallel arrays from print_task_config - auto filament_exist = ptc.value("filament_exist", std::vector{}); - auto filament_type = ptc.value("filament_type", std::vector{}); - auto filament_sub_type = ptc.value("filament_sub_type", std::vector{}); - auto filament_color = ptc.value("filament_color_rgba", std::vector{}); - auto filament_vendor = ptc.value("filament_vendor", std::vector{}); - - const int slot_count = static_cast(filament_exist.size()); - if (slot_count == 0) { - BOOST_LOG_TRIVIAL(info) << "SnapmakerPrinterAgent::fetch_filament_info: No filament slots reported"; - return false; - } - - // Read NFC filament_detect data for temperature info (optional) - nlohmann::json nfc_info; - if (json["result"]["status"].contains("filament_detect") && - json["result"]["status"]["filament_detect"].contains("info")) { - nfc_info = json["result"]["status"]["filament_detect"]["info"]; - } - - static const std::string empty_str; - static const std::string default_color = "FFFFFFFF"; - - std::vector trays; - trays.reserve(slot_count); - - for (int i = 0; i < slot_count; ++i) { - AmsTrayData tray; - tray.slot_index = i; - tray.has_filament = filament_exist[i]; - - if (tray.has_filament) { - tray.tray_type = combine_filament_type(safe_at(filament_type, i, empty_str), - safe_at(filament_sub_type, i, empty_str)); - tray.tray_color = safe_at(filament_color, i, default_color); - - auto* bundle = GUI::wxGetApp().preset_bundle; - // Try to find a matching preset for this filament based on vendor, type and color. - // If not found, default to traditional search by type only or generic type mapping. - if (bundle) { - std::string vendor = safe_at(filament_vendor, i, empty_str); - std::string filament_id = find_closest_color_preset_by_vendor_and_type(bundle->filaments, vendor, tray.tray_type, - tray.tray_color); - - if (!filament_id.empty()) { - tray.tray_info_idx = filament_id; - BOOST_LOG_TRIVIAL(warning) << "Filament sync: Found manufacturer-specific profile for slot " << i << ": " - << filament_id; + auto http = Http::get(url); + if (!api_key.empty()) { + http.header("X-Api-Key", api_key); + } + http.timeout_connect(5) + .timeout_max(10) + .on_complete([&](std::string body, unsigned status) { + if (status == 200) { + response_body = body; + success = true; } else { - tray.tray_info_idx = bundle->filaments.filament_id_by_type(tray.tray_type); + http_error = "HTTP error: " + std::to_string(status); } - } else { - tray.tray_info_idx = map_filament_type_to_generic_id(tray.tray_type); - } + }) + .on_error([&](std::string body, std::string err, unsigned status) { + http_error = err; + if (status > 0) { + http_error += " (HTTP " + std::to_string(status) + ")"; + } + }) + .perform_sync(); - // Extract NFC temperature data if available - if (nfc_info.is_array() && i < static_cast(nfc_info.size()) && nfc_info[i].is_object()) { - auto& nfc_slot = nfc_info[i]; - std::string vendor = nfc_slot.value("VENDOR", "NONE"); - if (vendor != "NONE" && !vendor.empty()) { - tray.bed_temp = nfc_slot.value("BED_TEMP", 0); - tray.nozzle_temp = nfc_slot.value("FIRST_LAYER_TEMP", 0); - } - } + if (!success) { + BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: HTTP request failed: " << http_error; + return; } - trays.emplace_back(std::move(tray)); - } + auto json = nlohmann::json::parse(response_body, nullptr, false, true); + if (json.is_discarded()) { + BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: Invalid JSON response"; + return; + } + + // Navigate to result.status.print_task_config + if (!json.contains("result") || !json["result"].contains("status") || !json["result"]["status"].contains("print_task_config")) { + BOOST_LOG_TRIVIAL(warning) << "SnapmakerPrinterAgent::fetch_filament_info: Missing print_task_config in response"; + return; + } + + auto& ptc = json["result"]["status"]["print_task_config"]; + + // Read parallel arrays from print_task_config + auto filament_exist = ptc.value("filament_exist", std::vector{}); + auto filament_type = ptc.value("filament_type", std::vector{}); + auto filament_sub_type = ptc.value("filament_sub_type", std::vector{}); + auto filament_color = ptc.value("filament_color_rgba", std::vector{}); + auto filament_vendor = ptc.value("filament_vendor", std::vector{}); + + const int slot_count = static_cast(filament_exist.size()); + if (slot_count == 0) { + BOOST_LOG_TRIVIAL(info) << "SnapmakerPrinterAgent::fetch_filament_info: No filament slots reported"; + return; + } + + // Read NFC filament_detect data for temperature info (optional) + nlohmann::json nfc_info; + if (json["result"]["status"].contains("filament_detect") && json["result"]["status"]["filament_detect"].contains("info")) { + nfc_info = json["result"]["status"]["filament_detect"]["info"]; + } + + static const std::string empty_str; + static const std::string default_color = "FFFFFFFF"; + + std::vector trays; + trays.reserve(slot_count); + + for (int i = 0; i < slot_count; ++i) { + AmsTrayData tray; + tray.slot_index = i; + tray.has_filament = filament_exist[i]; + + if (tray.has_filament) { + tray.tray_type = combine_filament_type(safe_at(filament_type, i, empty_str), safe_at(filament_sub_type, i, empty_str)); + tray.tray_color = safe_at(filament_color, i, default_color); + + auto* bundle = GUI::wxGetApp().preset_bundle; + // Try to find a matching preset for this filament based on vendor, type and color. + // If not found, default to traditional search by type only or generic type mapping. + if (bundle) { + std::string vendor = safe_at(filament_vendor, i, empty_str); + std::string filament_id = find_closest_color_preset_by_vendor_and_type(bundle->filaments, vendor, tray.tray_type, + tray.tray_color); + + if (!filament_id.empty()) { + tray.tray_info_idx = filament_id; + BOOST_LOG_TRIVIAL(warning) + << "Filament sync: Found manufacturer-specific profile for slot " << i << ": " << filament_id; + } else { + tray.tray_info_idx = bundle->filaments.filament_id_by_type(tray.tray_type); + } + } else { + tray.tray_info_idx = map_filament_type_to_generic_id(tray.tray_type); + } + + // Extract NFC temperature data if available + if (nfc_info.is_array() && i < static_cast(nfc_info.size()) && nfc_info[i].is_object()) { + auto& nfc_slot = nfc_info[i]; + std::string vendor = nfc_slot.value("VENDOR", "NONE"); + if (vendor != "NONE" && !vendor.empty()) { + tray.bed_temp = nfc_slot.value("BED_TEMP", 0); + tray.nozzle_temp = nfc_slot.value("FIRST_LAYER_TEMP", 0); + } + } + } + + trays.emplace_back(std::move(tray)); + } + + build_ams_payload(1, slot_count - 1, trays); + }).detach(); - build_ams_payload(1, slot_count - 1, trays); return true; } diff --git a/src/slic3r/Utils/SnapmakerPrinterAgent.hpp b/src/slic3r/Utils/SnapmakerPrinterAgent.hpp index 5f7b6b1c30..c1e35701c3 100644 --- a/src/slic3r/Utils/SnapmakerPrinterAgent.hpp +++ b/src/slic3r/Utils/SnapmakerPrinterAgent.hpp @@ -1,5 +1,6 @@ #pragma once +#include "IPrinterAgent.hpp" #include "MoonrakerPrinterAgent.hpp" #include @@ -21,6 +22,8 @@ public: FilamentSyncMode get_filament_sync_mode() const override; int connect_printer(std::string dev_id, std::string dev_ip, std::string username, std::string password, bool use_ssl) override; int command_start_camera(std::string dev_id) override; + CameraStreamMode get_camera_stream_mode() const override { return CameraStreamMode::http_snapshot; } + std::string get_camera_url() const override { return device_info.base_url + "/server/files/camera/monitor.jpg"; } private: // Combine filament_type + filament_sub_type into a unified type string diff --git a/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp b/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp index d428775c12..c9ea52e8bb 100644 --- a/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp +++ b/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp @@ -23,6 +23,14 @@ void PrinterAgentPluginCapability::RegisterBindings(pybind11::module_& module) .value("Pull", FilamentSyncMode::pull) .export_values(); + py::enum_(printer_agent_module, "CameraStreamMode") + .value("None_", CameraStreamMode::none) + .value("HTTP", CameraStreamMode::http) + .value("HTTP_SNAPSHOT", CameraStreamMode::http_snapshot) + .value("RTSP", CameraStreamMode::rtsp) + .value("WebRTC", CameraStreamMode::webrtc) + .export_values(); + py::class_(printer_agent_module, "AgentInfo") .def(py::init<>()) .def(py::init([](std::string id, std::string name, std::string version, std::string description) { @@ -105,7 +113,9 @@ void PrinterAgentPluginCapability::RegisterBindings(pybind11::module_& module) .def("start_send_gcode_to_sdcard", &PrinterAgentPluginCapability::start_send_gcode_to_sdcard) .def("start_local_print", &PrinterAgentPluginCapability::start_local_print) .def("get_filament_sync_mode", &PrinterAgentPluginCapability::get_filament_sync_mode) + .def("get_camera_stream_mode", &PrinterAgentPluginCapability::get_camera_stream_mode) .def("fetch_filament_info", &PrinterAgentPluginCapability::fetch_filament_info) + .def("get_camera_url", &PrinterAgentPluginCapability::get_camera_url) .def("check_cert", &PrinterAgentPluginCapability::check_cert) .def("install_device_cert", &PrinterAgentPluginCapability::install_device_cert) .def("ping_bind", &PrinterAgentPluginCapability::ping_bind) diff --git a/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp b/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp index af869b68f3..ae602ca38c 100644 --- a/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp +++ b/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp @@ -41,6 +41,8 @@ public: int start_local_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn) override = 0; FilamentSyncMode get_filament_sync_mode() const override = 0; bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override = 0; + CameraStreamMode get_camera_stream_mode() const override = 0; + std::string get_camera_url() const override = 0; int check_cert() override = 0; void install_device_cert(std::string dev_id, bool lan_only) override = 0; diff --git a/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapabilityTrampoline.hpp b/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapabilityTrampoline.hpp index 7e017ca55e..88051ed649 100644 --- a/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapabilityTrampoline.hpp +++ b/src/slic3r/plugin/pluginTypes/printerAgent/PrinterAgentPluginCapabilityTrampoline.hpp @@ -5,7 +5,10 @@ #include "../../PyPluginTrampoline.hpp" #include "IPrinterAgent.hpp" +#include "pybind11/pybind11.h" +#include #include +#include namespace Slic3r { class PyPrinterAgentPluginCapabilityTrampoline : public PyPluginCommonTrampoline @@ -96,6 +99,19 @@ public: get_filament_sync_mode); } + CameraStreamMode get_camera_stream_mode() const override + { + ORCA_PY_OVERRIDE_AUDITED( + ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, CameraStreamMode, PrinterAgentPluginCapability, + get_camera_stream_mode); + } + + std::string get_camera_url() const override + { + ORCA_PY_OVERRIDE_AUDITED( + ::Slic3r::PluginAuditManager::AuditMode::Loading, [] {}, PYBIND11_OVERRIDE_PURE, std::string, PrinterAgentPluginCapability, get_camera_url); + } + bool fetch_filament_info(std::string dev_id, FilamentSyncMode sync_mode = FilamentSyncMode::pull) override { ORCA_PY_OVERRIDE_AUDITED(