This commit is contained in:
Maciej Wilczyński 2025-12-23 16:57:40 -05:00 committed by GitHub
commit dc4529f1de
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 3497 additions and 241 deletions

View file

@ -683,6 +683,19 @@ std::string AppConfig::load()
local_machine.printer_type = p["printer_type"].get<std::string>();
m_local_machines[local_machine.dev_id] = local_machine;
}
} else if (it.key() == "printer_cameras") {
for (auto m = it.value().begin(); m != it.value().end(); ++m) {
const auto& p = m.value();
PrinterCameraConfig cam_config;
cam_config.dev_id = m.key();
if (p.contains("custom_source"))
cam_config.custom_source = p["custom_source"].get<std::string>();
if (p.contains("source_type"))
cam_config.source_type = camera_source_type_from_string(p["source_type"].get<std::string>());
if (p.contains("enabled"))
cam_config.enabled = p["enabled"].get<bool>();
m_printer_cameras[cam_config.dev_id] = cam_config;
}
} else {
if (it.value().is_object()) {
for (auto iter = it.value().begin(); iter != it.value().end(); iter++) {
@ -880,6 +893,14 @@ void AppConfig::save()
j["local_machines"][local_machine.first] = m_json;
}
for (const auto& printer_camera : m_printer_cameras) {
json cam_json;
cam_json["custom_source"] = printer_camera.second.custom_source;
cam_json["source_type"] = camera_source_type_to_string(printer_camera.second.source_type);
cam_json["enabled"] = printer_camera.second.enabled;
j["printer_cameras"][printer_camera.first] = cam_json;
}
boost::nowide::ofstream c;
c.open(path_pid, std::ios::out | std::ios::trunc);
c << std::setw(4) << j << std::endl;

View file

@ -46,6 +46,45 @@ struct BBLocalMachine
bool operator!=(const BBLocalMachine& other) const { return !operator==(other); }
};
enum class CameraSourceType : int {
Builtin = 0,
WebView = 1,
RTSP = 2,
MJPEG = 3
};
inline std::string camera_source_type_to_string(CameraSourceType type) {
switch (type) {
case CameraSourceType::Builtin: return "builtin";
case CameraSourceType::WebView: return "webview";
case CameraSourceType::RTSP: return "rtsp";
case CameraSourceType::MJPEG: return "mjpeg";
}
return "builtin";
}
inline CameraSourceType camera_source_type_from_string(const std::string& s) {
if (s == "webview") return CameraSourceType::WebView;
if (s == "rtsp") return CameraSourceType::RTSP;
if (s == "mjpeg") return CameraSourceType::MJPEG;
return CameraSourceType::Builtin;
}
struct PrinterCameraConfig
{
std::string dev_id;
std::string custom_source;
CameraSourceType source_type = CameraSourceType::Builtin;
bool enabled = false;
bool operator==(const PrinterCameraConfig& other) const
{
return dev_id == other.dev_id && custom_source == other.custom_source
&& source_type == other.source_type && enabled == other.enabled;
}
bool operator!=(const PrinterCameraConfig& other) const { return !operator==(other); }
};
class AppConfig
{
public:
@ -246,6 +285,40 @@ public:
}
}
const std::map<std::string, PrinterCameraConfig>& get_all_printer_cameras() const { return m_printer_cameras; }
bool has_printer_camera(const std::string& dev_id) const
{
return m_printer_cameras.find(dev_id) != m_printer_cameras.end();
}
PrinterCameraConfig get_printer_camera(const std::string& dev_id) const
{
auto it = m_printer_cameras.find(dev_id);
if (it != m_printer_cameras.end())
return it->second;
return PrinterCameraConfig{};
}
void set_printer_camera(const PrinterCameraConfig& config)
{
auto it = m_printer_cameras.find(config.dev_id);
if (it != m_printer_cameras.end()) {
if (it->second != config) {
m_printer_cameras[config.dev_id] = config;
m_dirty = true;
}
} else {
m_printer_cameras[config.dev_id] = config;
m_dirty = true;
}
}
void erase_printer_camera(const std::string& dev_id)
{
auto it = m_printer_cameras.find(dev_id);
if (it != m_printer_cameras.end()) {
m_printer_cameras.erase(it);
m_dirty = true;
}
}
const std::vector<std::string> &get_filament_presets() const { return m_filament_presets; }
void set_filament_presets(const std::vector<std::string> &filament_presets){
m_filament_presets = filament_presets;
@ -389,6 +462,7 @@ private:
std::vector<PrinterCaliInfo> m_printer_cali_infos;
std::map<std::string, BBLocalMachine> m_local_machines;
std::map<std::string, PrinterCameraConfig> m_printer_cameras;
};
} // namespace Slic3r

View file

@ -80,6 +80,8 @@ set(SLIC3R_GUI_SOURCES
GUI/Camera.hpp
GUI/CameraPopup.cpp
GUI/CameraPopup.hpp
GUI/CameraManagementDialog.cpp
GUI/CameraManagementDialog.hpp
GUI/CameraUtils.cpp
GUI/CameraUtils.hpp
GUI/CloneDialog.cpp
@ -650,18 +652,25 @@ if (APPLE)
GUI/GUI_UtilsMac.mm
GUI/wxMediaCtrl2.mm
GUI/wxMediaCtrl2.h
GUI/NativeMediaCtrl.h
GUI/NativeMediaCtrl.cpp
GUI/NativeMediaCtrl_Mac.mm
)
FIND_LIBRARY(DISKARBITRATION_LIBRARY DiskArbitration)
else ()
list(APPEND SLIC3R_GUI_SOURCES
GUI/wxMediaCtrl2.cpp
GUI/wxMediaCtrl2.h
GUI/NativeMediaCtrl.h
GUI/NativeMediaCtrl.cpp
GUI/NativeMediaCtrl_Win.cpp
)
endif ()
if (UNIX AND NOT APPLE)
list(APPEND SLIC3R_GUI_SOURCES
GUI/Printer/gstbambusrc.c
GUI/NativeMediaCtrl_Linux.cpp
)
endif ()
@ -764,8 +773,19 @@ if (UNIX AND NOT APPLE)
# We add GStreamer for bambu:/// support.
pkg_check_modules(GSTREAMER REQUIRED gstreamer-1.0)
pkg_check_modules(GST_BASE REQUIRED gstreamer-base-1.0)
target_link_libraries(libslic3r_gui ${GSTREAMER_LIBRARIES} ${GST_BASE_LIBRARIES})
target_include_directories(libslic3r_gui SYSTEM PRIVATE ${GSTREAMER_INCLUDE_DIRS} ${GST_BASE_INCLUDE_DIRS})
pkg_check_modules(GST_VIDEO REQUIRED gstreamer-video-1.0)
target_link_libraries(libslic3r_gui ${GSTREAMER_LIBRARIES} ${GST_BASE_LIBRARIES} ${GST_VIDEO_LIBRARIES})
target_include_directories(libslic3r_gui SYSTEM PRIVATE ${GSTREAMER_INCLUDE_DIRS} ${GST_BASE_INCLUDE_DIRS} ${GST_VIDEO_INCLUDE_DIRS})
endif ()
if (APPLE)
find_package(PkgConfig REQUIRED)
pkg_check_modules(GSTREAMER REQUIRED gstreamer-1.0)
pkg_check_modules(GST_BASE REQUIRED gstreamer-base-1.0)
pkg_check_modules(GST_VIDEO REQUIRED gstreamer-video-1.0)
pkg_check_modules(GST_APP REQUIRED gstreamer-app-1.0)
target_link_libraries(libslic3r_gui ${GSTREAMER_LDFLAGS} ${GST_BASE_LDFLAGS} ${GST_VIDEO_LDFLAGS} ${GST_APP_LDFLAGS})
target_include_directories(libslic3r_gui SYSTEM PRIVATE ${GSTREAMER_INCLUDE_DIRS} ${GST_BASE_INCLUDE_DIRS} ${GST_VIDEO_INCLUDE_DIRS} ${GST_APP_INCLUDE_DIRS})
endif ()
# Add a definition so that we can tell we are compiling slic3r.

View file

@ -0,0 +1,417 @@
#include "CameraManagementDialog.hpp"
#include "I18N.hpp"
#include "GUI_App.hpp"
#include "Widgets/Label.hpp"
#include "Widgets/DialogButtons.hpp"
#include "DeviceCore/DevManager.h"
namespace Slic3r { namespace GUI {
CameraEditDialog::CameraEditDialog(wxWindow* parent,
const std::string& dev_id,
const std::string& url,
CameraSourceType source_type,
bool enabled)
: DPIDialog(parent, wxID_ANY, _L("Edit Camera Override"), wxDefaultPosition, wxSize(FromDIP(400), FromDIP(250)), wxDEFAULT_DIALOG_STYLE)
, m_initial_dev_id(dev_id)
{
create_ui();
populate_printer_list();
if (!dev_id.empty()) {
for (size_t i = 0; i < m_printers.size(); ++i) {
if (m_printers[i].first == dev_id) {
m_printer_combo->SetSelection(static_cast<int>(i));
break;
}
}
}
m_source_type_combo->SetSelection(static_cast<int>(source_type));
m_url_input->GetTextCtrl()->SetValue(url);
m_enabled_checkbox->SetValue(enabled);
update_url_field_state();
wxGetApp().UpdateDarkUIWin(this);
}
void CameraEditDialog::create_ui()
{
SetBackgroundColour(*wxWHITE);
wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL);
wxFlexGridSizer* grid_sizer = new wxFlexGridSizer(4, 2, FromDIP(10), FromDIP(10));
grid_sizer->AddGrowableCol(1);
wxStaticText* printer_label = new wxStaticText(this, wxID_ANY, _L("Printer:"));
m_printer_combo = new wxComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(250), -1), 0, nullptr, wxCB_READONLY);
wxStaticText* source_type_label = new wxStaticText(this, wxID_ANY, _L("Source Type:"));
wxArrayString source_types;
source_types.Add(_L("Built-in Camera"));
source_types.Add(_L("Web View"));
source_types.Add(_L("RTSP Stream"));
source_types.Add(_L("MJPEG Stream"));
m_source_type_combo = new wxComboBox(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(250), -1), source_types, wxCB_READONLY);
m_source_type_combo->SetSelection(0);
m_source_type_combo->Bind(wxEVT_COMBOBOX, &CameraEditDialog::on_source_type_changed, this);
wxStaticText* url_label = new wxStaticText(this, wxID_ANY, _L("Camera URL:"));
m_url_input = new TextInput(this, wxEmptyString, wxEmptyString, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(250), -1));
m_url_input->GetTextCtrl()->SetHint(_L("rtsp://user:pass@camera.local:554/stream"));
wxStaticText* enabled_label = new wxStaticText(this, wxID_ANY, _L("Enabled:"));
m_enabled_checkbox = new CheckBox(this);
grid_sizer->Add(printer_label, 0, wxALIGN_CENTER_VERTICAL);
grid_sizer->Add(m_printer_combo, 1, wxEXPAND);
grid_sizer->Add(source_type_label, 0, wxALIGN_CENTER_VERTICAL);
grid_sizer->Add(m_source_type_combo, 1, wxEXPAND);
grid_sizer->Add(url_label, 0, wxALIGN_CENTER_VERTICAL);
grid_sizer->Add(m_url_input, 1, wxEXPAND);
grid_sizer->Add(enabled_label, 0, wxALIGN_CENTER_VERTICAL);
grid_sizer->Add(m_enabled_checkbox, 0);
main_sizer->Add(grid_sizer, 0, wxEXPAND | wxALL, FromDIP(15));
auto dlg_btns = new DialogButtons(this, {"OK", "Cancel"});
main_sizer->Add(dlg_btns, 0, wxEXPAND);
dlg_btns->GetOK()->Bind(wxEVT_BUTTON, &CameraEditDialog::on_ok, this);
dlg_btns->GetCANCEL()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_CANCEL); });
SetSizer(main_sizer);
Layout();
Fit();
CenterOnParent();
}
void CameraEditDialog::populate_printer_list()
{
m_printers.clear();
const auto& existing_cameras = wxGetApp().app_config->get_all_printer_cameras();
auto has_existing_override = [&](const std::string& dev_id) {
if (!m_initial_dev_id.empty()) return false;
return existing_cameras.find(dev_id) != existing_cameras.end();
};
auto* dev_manager = wxGetApp().getDeviceManager();
if (dev_manager) {
for (const auto& pair : dev_manager->get_local_machinelist()) {
if (pair.second && !has_existing_override(pair.second->get_dev_id())) {
m_printers.emplace_back(pair.second->get_dev_id(), pair.second->get_dev_name());
}
}
for (const auto& pair : dev_manager->get_user_machinelist()) {
if (pair.second && !has_existing_override(pair.second->get_dev_id())) {
bool exists = false;
for (const auto& p : m_printers) {
if (p.first == pair.second->get_dev_id()) {
exists = true;
break;
}
}
if (!exists) {
m_printers.emplace_back(pair.second->get_dev_id(), pair.second->get_dev_name());
}
}
}
}
const auto& local_machines = wxGetApp().app_config->get_local_machines();
for (const auto& pair : local_machines) {
if (has_existing_override(pair.first)) continue;
bool exists = false;
for (const auto& p : m_printers) {
if (p.first == pair.first) {
exists = true;
break;
}
}
if (!exists) {
m_printers.emplace_back(pair.first, pair.second.dev_name);
}
}
m_printer_combo->Clear();
for (const auto& p : m_printers) {
wxString display = wxString::Format("%s (%s)", p.second, p.first);
m_printer_combo->Append(display);
}
if (!m_printers.empty()) {
m_printer_combo->SetSelection(0);
}
}
void CameraEditDialog::on_ok(wxCommandEvent& event)
{
if (m_printer_combo->GetSelection() == wxNOT_FOUND) {
wxMessageBox(_L("Please select a printer"), _L("Error"), wxOK | wxICON_ERROR, this);
return;
}
CameraSourceType type = get_source_type();
if (type != CameraSourceType::Builtin && m_url_input->GetTextCtrl()->GetValue().IsEmpty()) {
wxMessageBox(_L("Please enter a camera URL"), _L("Error"), wxOK | wxICON_ERROR, this);
return;
}
EndModal(wxID_OK);
}
std::string CameraEditDialog::get_dev_id() const
{
int sel = m_printer_combo->GetSelection();
if (sel >= 0 && sel < static_cast<int>(m_printers.size())) {
return m_printers[sel].first;
}
return "";
}
std::string CameraEditDialog::get_url() const
{
return m_url_input->GetTextCtrl()->GetValue().ToStdString();
}
CameraSourceType CameraEditDialog::get_source_type() const
{
int sel = m_source_type_combo->GetSelection();
if (sel >= 0 && sel <= static_cast<int>(CameraSourceType::MJPEG)) {
return static_cast<CameraSourceType>(sel);
}
return CameraSourceType::Builtin;
}
bool CameraEditDialog::get_enabled() const
{
return m_enabled_checkbox->GetValue();
}
void CameraEditDialog::on_source_type_changed(wxCommandEvent& event)
{
update_url_field_state();
}
void CameraEditDialog::update_url_field_state()
{
CameraSourceType type = get_source_type();
bool needs_url = (type != CameraSourceType::Builtin);
m_url_input->Enable(needs_url);
if (type == CameraSourceType::RTSP) {
m_url_input->GetTextCtrl()->SetHint(_L("rtsp://user:pass@camera.local:554/stream"));
} else if (type == CameraSourceType::MJPEG) {
m_url_input->GetTextCtrl()->SetHint(_L("http://camera.local/mjpg/video.mjpg"));
} else if (type == CameraSourceType::WebView) {
m_url_input->GetTextCtrl()->SetHint(_L("http://camera.local/stream"));
} else {
m_url_input->GetTextCtrl()->SetHint(wxEmptyString);
}
}
void CameraEditDialog::on_dpi_changed(const wxRect& suggested_rect)
{
Layout();
Fit();
}
CameraManagementDialog::CameraManagementDialog(wxWindow* parent)
: DPIDialog(parent, wxID_ANY, _L("Camera Overrides"), wxDefaultPosition, wxSize(FromDIP(550), FromDIP(400)), wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER)
{
create_ui();
refresh_list();
wxGetApp().UpdateDarkUIWin(this);
}
CameraManagementDialog::~CameraManagementDialog()
{
cleanup_list_data();
}
void CameraManagementDialog::create_ui()
{
SetBackgroundColour(*wxWHITE);
wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL);
wxStaticText* title = new wxStaticText(this, wxID_ANY, _L("Configure custom camera URLs for each printer"));
title->SetFont(Label::Body_14);
main_sizer->Add(title, 0, wxALL, FromDIP(15));
m_list_ctrl = new wxDataViewListCtrl(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxDV_SINGLE | wxDV_ROW_LINES);
m_list_ctrl->AppendTextColumn(_L("Printer"), wxDATAVIEW_CELL_INERT, FromDIP(120));
m_list_ctrl->AppendTextColumn(_L("Serial"), wxDATAVIEW_CELL_INERT, FromDIP(80));
m_list_ctrl->AppendTextColumn(_L("Type"), wxDATAVIEW_CELL_INERT, FromDIP(70));
m_list_ctrl->AppendTextColumn(_L("Camera URL"), wxDATAVIEW_CELL_INERT, FromDIP(180));
m_list_ctrl->AppendTextColumn(_L("Enabled"), wxDATAVIEW_CELL_INERT, FromDIP(60));
m_list_ctrl->Bind(wxEVT_DATAVIEW_SELECTION_CHANGED, &CameraManagementDialog::on_selection_changed, this);
m_list_ctrl->Bind(wxEVT_DATAVIEW_ITEM_ACTIVATED, &CameraManagementDialog::on_item_activated, this);
main_sizer->Add(m_list_ctrl, 1, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(15));
auto dlg_btns = new DialogButtons(this, {"Delete", "Add", "Edit", "Close"}, "", 1);
main_sizer->Add(dlg_btns, 0, wxEXPAND);
m_btn_delete = dlg_btns->GetButtonFromLabel(_L("Delete"));
m_btn_add = dlg_btns->GetButtonFromLabel(_L("Add"));
m_btn_edit = dlg_btns->GetButtonFromLabel(_L("Edit"));
m_btn_delete->Enable(false);
m_btn_edit->Enable(false);
m_btn_add->Bind(wxEVT_BUTTON, &CameraManagementDialog::on_add, this);
m_btn_edit->Bind(wxEVT_BUTTON, &CameraManagementDialog::on_edit, this);
m_btn_delete->Bind(wxEVT_BUTTON, &CameraManagementDialog::on_delete, this);
dlg_btns->GetButtonFromLabel(_L("Close"))->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_OK); });
dlg_btns->SetAlertButton(_L("Delete"));
SetSizer(main_sizer);
Layout();
CenterOnParent();
}
static wxString source_type_display_name(CameraSourceType type) {
switch (type) {
case CameraSourceType::Builtin: return _L("Built-in");
case CameraSourceType::WebView: return _L("WebView");
case CameraSourceType::RTSP: return _L("RTSP");
case CameraSourceType::MJPEG: return _L("MJPEG");
}
return _L("Built-in");
}
void CameraManagementDialog::cleanup_list_data()
{
if (!m_list_ctrl) return;
for (unsigned int i = 0; i < m_list_ctrl->GetItemCount(); ++i) {
wxDataViewItem item = m_list_ctrl->RowToItem(i);
auto* dev_id_ptr = reinterpret_cast<std::string*>(m_list_ctrl->GetItemData(item));
delete dev_id_ptr;
}
}
void CameraManagementDialog::refresh_list()
{
cleanup_list_data();
m_list_ctrl->DeleteAllItems();
const auto& cameras = wxGetApp().app_config->get_all_printer_cameras();
for (const auto& pair : cameras) {
wxVector<wxVariant> data;
data.push_back(wxVariant(get_printer_name_for_dev_id(pair.first)));
data.push_back(wxVariant(truncate_serial(pair.first)));
data.push_back(wxVariant(source_type_display_name(pair.second.source_type)));
data.push_back(wxVariant(pair.second.custom_source));
data.push_back(wxVariant(pair.second.enabled ? _L("Yes") : _L("No")));
m_list_ctrl->AppendItem(data, reinterpret_cast<wxUIntPtr>(new std::string(pair.first)));
}
m_btn_edit->Enable(false);
m_btn_delete->Enable(false);
}
void CameraManagementDialog::on_add(wxCommandEvent& event)
{
CameraEditDialog dlg(this);
if (dlg.ShowModal() == wxID_OK) {
PrinterCameraConfig config;
config.dev_id = dlg.get_dev_id();
config.custom_source = dlg.get_url();
config.source_type = dlg.get_source_type();
config.enabled = dlg.get_enabled();
wxGetApp().app_config->set_printer_camera(config);
refresh_list();
}
}
void CameraManagementDialog::on_edit(wxCommandEvent& event)
{
int row = m_list_ctrl->GetSelectedRow();
if (row == wxNOT_FOUND) return;
auto* dev_id_ptr = reinterpret_cast<std::string*>(m_list_ctrl->GetItemData(m_list_ctrl->RowToItem(row)));
if (!dev_id_ptr) return;
std::string dev_id = *dev_id_ptr;
auto config = wxGetApp().app_config->get_printer_camera(dev_id);
CameraEditDialog dlg(this, dev_id, config.custom_source, config.source_type, config.enabled);
if (dlg.ShowModal() == wxID_OK) {
if (dlg.get_dev_id() != dev_id) {
wxGetApp().app_config->erase_printer_camera(dev_id);
}
PrinterCameraConfig new_config;
new_config.dev_id = dlg.get_dev_id();
new_config.custom_source = dlg.get_url();
new_config.source_type = dlg.get_source_type();
new_config.enabled = dlg.get_enabled();
wxGetApp().app_config->set_printer_camera(new_config);
refresh_list();
}
}
void CameraManagementDialog::on_delete(wxCommandEvent& event)
{
int row = m_list_ctrl->GetSelectedRow();
if (row == wxNOT_FOUND) return;
auto* dev_id_ptr = reinterpret_cast<std::string*>(m_list_ctrl->GetItemData(m_list_ctrl->RowToItem(row)));
if (!dev_id_ptr) return;
int result = wxMessageBox(_L("Are you sure you want to delete this camera override?"), _L("Confirm Delete"), wxYES_NO | wxICON_QUESTION, this);
if (result == wxYES) {
wxGetApp().app_config->erase_printer_camera(*dev_id_ptr);
refresh_list();
}
}
void CameraManagementDialog::on_selection_changed(wxDataViewEvent& event)
{
bool has_selection = m_list_ctrl->GetSelectedRow() != wxNOT_FOUND;
m_btn_edit->Enable(has_selection);
m_btn_delete->Enable(has_selection);
}
void CameraManagementDialog::on_item_activated(wxDataViewEvent& event)
{
wxCommandEvent evt;
on_edit(evt);
}
std::string CameraManagementDialog::get_printer_name_for_dev_id(const std::string& dev_id)
{
auto* dev_manager = wxGetApp().getDeviceManager();
if (dev_manager) {
MachineObject* obj = dev_manager->get_local_machine(dev_id);
if (obj) return obj->get_dev_name();
obj = dev_manager->get_user_machine(dev_id);
if (obj) return obj->get_dev_name();
}
const auto& local_machines = wxGetApp().app_config->get_local_machines();
auto it = local_machines.find(dev_id);
if (it != local_machines.end()) {
return it->second.dev_name;
}
return truncate_serial(dev_id);
}
std::string CameraManagementDialog::truncate_serial(const std::string& dev_id)
{
return dev_id;
}
void CameraManagementDialog::on_dpi_changed(const wxRect& suggested_rect)
{
Layout();
}
}} // namespace Slic3r::GUI

View file

@ -0,0 +1,76 @@
#ifndef slic3r_CameraManagementDialog_hpp_
#define slic3r_CameraManagementDialog_hpp_
#include "GUI_Utils.hpp"
#include "Widgets/Button.hpp"
#include "Widgets/TextInput.hpp"
#include "Widgets/CheckBox.hpp"
#include "libslic3r/AppConfig.hpp"
#include <wx/dataview.h>
namespace Slic3r { namespace GUI {
class CameraEditDialog : public DPIDialog
{
public:
CameraEditDialog(wxWindow* parent,
const std::string& dev_id = "",
const std::string& url = "",
CameraSourceType source_type = CameraSourceType::Builtin,
bool enabled = false);
std::string get_dev_id() const;
std::string get_url() const;
CameraSourceType get_source_type() const;
bool get_enabled() const;
protected:
void on_dpi_changed(const wxRect& suggested_rect) override;
private:
void create_ui();
void populate_printer_list();
void on_ok(wxCommandEvent& event);
void on_source_type_changed(wxCommandEvent& event);
void update_url_field_state();
wxComboBox* m_printer_combo{nullptr};
wxComboBox* m_source_type_combo{nullptr};
TextInput* m_url_input{nullptr};
CheckBox* m_enabled_checkbox{nullptr};
std::vector<std::pair<std::string, std::string>> m_printers;
std::string m_initial_dev_id;
};
class CameraManagementDialog : public DPIDialog
{
public:
CameraManagementDialog(wxWindow* parent);
~CameraManagementDialog();
protected:
void on_dpi_changed(const wxRect& suggested_rect) override;
private:
void create_ui();
void refresh_list();
void cleanup_list_data();
void on_add(wxCommandEvent& event);
void on_edit(wxCommandEvent& event);
void on_delete(wxCommandEvent& event);
void on_selection_changed(wxDataViewEvent& event);
void on_item_activated(wxDataViewEvent& event);
std::string get_printer_name_for_dev_id(const std::string& dev_id);
std::string truncate_serial(const std::string& dev_id);
wxDataViewListCtrl* m_list_ctrl{nullptr};
Button* m_btn_add{nullptr};
Button* m_btn_edit{nullptr};
Button* m_btn_delete{nullptr};
};
}} // namespace Slic3r::GUI
#endif // slic3r_CameraManagementDialog_hpp_

View file

@ -3,11 +3,14 @@
#include "I18N.hpp"
#include "Widgets/Label.hpp"
#include "libslic3r/Utils.hpp"
#include "libslic3r/AppConfig.hpp"
#include "BitmapCache.hpp"
#include <wx/progdlg.h>
#include <wx/clipbrd.h>
#include <wx/dcgraph.h>
#include "GUI_App.hpp"
#include "MainFrame.hpp"
#include "CameraManagementDialog.hpp"
#include <slic3r/GUI/StatusPanel.hpp>
#include "DeviceCore/DevManager.h"
@ -16,33 +19,23 @@
namespace Slic3r {
namespace GUI {
wxIMPLEMENT_CLASS(CameraPopup, PopupWindow);
wxBEGIN_EVENT_TABLE(CameraPopup, PopupWindow)
EVT_MOUSE_EVENTS(CameraPopup::OnMouse )
EVT_SIZE(CameraPopup::OnSize)
EVT_SET_FOCUS(CameraPopup::OnSetFocus )
EVT_KILL_FOCUS(CameraPopup::OnKillFocus )
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
const wxColour TEXT_COL = wxColour(43, 52, 54);
CameraPopup::CameraPopup(wxWindow *parent)
: PopupWindow(parent, wxBORDER_NONE | wxPU_CONTAINS_CONTROLS)
: DPIDialog(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize,
wxBORDER_SIMPLE | wxFRAME_NO_TASKBAR | wxFRAME_FLOAT_ON_PARENT | wxSTAY_ON_TOP)
{
SetBackgroundColour(*wxWHITE);
#ifdef __WINDOWS__
SetDoubleBuffered(true);
#endif
m_panel = new wxScrolledWindow(this, wxID_ANY);
m_panel->SetBackgroundColour(*wxWHITE);
m_panel->SetMinSize(wxSize(FromDIP(180),-1));
m_panel->Bind(wxEVT_MOTION, &CameraPopup::OnMouse, this);
main_sizer = new wxBoxSizer(wxVERTICAL);
wxFlexGridSizer* top_sizer = new wxFlexGridSizer(0, 2, 0, FromDIP(50));
@ -101,34 +94,24 @@ CameraPopup::CameraPopup(wxWindow *parent)
top_sizer->Add(0, 0, wxALL, 0);
}
// custom IP camera
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 = new wxStaticText(m_panel, wxID_ANY, _L("Custom camera"));
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");
}
m_switch_custom_camera = new SwitchButton(m_panel);
m_switch_custom_camera->Bind(wxEVT_TOGGLEBUTTON, &CameraPopup::on_custom_camera_switch_toggled, this);
top_sizer->Add(m_custom_camera_hint, 0, wxALIGN_CENTER_VERTICAL | wxALIGN_LEFT | wxALL, FromDIP(5));
top_sizer->Add(m_switch_custom_camera, 0, wxALIGN_CENTER_VERTICAL | wxALIGN_RIGHT | wxALL, FromDIP(5));
m_manage_cameras_link = new Label(m_panel, _L("Manage all camera overrides..."));
m_manage_cameras_link->SetForegroundColour(wxColour(0x1F, 0x8E, 0xEA));
m_manage_cameras_link->SetFont(Label::Body_12);
m_manage_cameras_link->SetCursor(wxCursor(wxCURSOR_HAND));
m_manage_cameras_link->Bind(wxEVT_LEFT_DOWN, &CameraPopup::on_manage_cameras_clicked, this);
top_sizer->Add(m_manage_cameras_link, 0, wxALIGN_LEFT | wxALL, FromDIP(5));
top_sizer->Add(0, 0, wxALL, 0);
top_sizer->Add(m_custom_camera_input, 2, wxALIGN_CENTER_VERTICAL | wxALIGN_LEFT | 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::Format(L"https://wiki.bambulab.com/%s/software/bambu-studio/virtual-camera", L"en");
@ -165,13 +148,13 @@ CameraPopup::CameraPopup(wxWindow *parent)
evt.SetEventObject(this);
GetEventHandler()->ProcessEvent(evt);
});
#ifdef __APPLE__
m_panel->Bind(wxEVT_LEFT_UP, &CameraPopup::OnLeftUp, this);
#endif //APPLE
this->Bind(wxEVT_TIMER, &CameraPopup::stop_interval, this);
m_interval_timer = new wxTimer();
m_interval_timer->SetOwner(this);
Bind(wxEVT_ACTIVATE, [this](wxActivateEvent& e) {
if (!e.GetActive()) {
Hide();
}
e.Skip();
});
wxGetApp().UpdateDarkUIWin(this);
}
@ -183,35 +166,55 @@ void CameraPopup::sdcard_absent_hint()
GetEventHandler()->ProcessEvent(evt);
}
void CameraPopup::on_camera_source_changed(wxCommandEvent &event)
void CameraPopup::on_custom_camera_switch_toggled(wxCommandEvent& event)
{
if (m_obj && !m_custom_camera_input->GetTextCtrl()->IsEmpty()) {
handle_camera_source_change();
}
}
if (!m_obj) return;
void CameraPopup::handle_camera_source_change()
{
m_custom_camera_enabled = !m_custom_camera_enabled;
std::string dev_id = m_obj->get_dev_id();
if (!wxGetApp().app_config->has_printer_camera(dev_id))
return;
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);
auto config = wxGetApp().app_config->get_printer_camera(dev_id);
config.enabled = m_switch_custom_camera->GetValue();
wxGetApp().app_config->set_printer_camera(config);
wxCommandEvent evt(EVT_CAM_SOURCE_CHANGE);
evt.SetEventObject(this);
GetEventHandler()->ProcessEvent(evt);
}
void CameraPopup::set_custom_cam_button_state(bool state)
void CameraPopup::update_custom_camera_switch()
{
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));
if (!m_obj) {
m_switch_custom_camera->SetValue(false);
m_switch_custom_camera->Enable(false);
return;
}
std::string dev_id = m_obj->get_dev_id();
if (wxGetApp().app_config->has_printer_camera(dev_id)) {
auto config = wxGetApp().app_config->get_printer_camera(dev_id);
bool has_url = !config.custom_source.empty();
m_switch_custom_camera->Enable(has_url);
m_switch_custom_camera->SetValue(has_url && config.enabled);
} else {
m_switch_custom_camera->SetValue(false);
m_switch_custom_camera->Enable(false);
}
}
void CameraPopup::on_manage_cameras_clicked(wxMouseEvent& event)
{
Hide();
CameraManagementDialog dlg(wxGetApp().mainframe);
dlg.ShowModal();
update_custom_camera_switch();
wxCommandEvent evt(EVT_CAM_SOURCE_CHANGE);
evt.SetEventObject(this);
GetEventHandler()->ProcessEvent(evt);
}
void CameraPopup::on_switch_recording(wxCommandEvent& event)
@ -233,17 +236,6 @@ void CameraPopup::on_set_resolution()
m_obj->command_ipcam_resolution_set(to_resolution_msg_string(curr_sel_resolution));
}
void CameraPopup::Popup(wxWindow *WXUNUSED(focus))
{
wxPoint curr_position = this->GetPosition();
wxSize win_size = this->GetSize();
curr_position.x -= win_size.x;
this->SetPosition(curr_position);
if (!m_is_in_interval)
PopupWindow::Popup();
}
wxWindow* CameraPopup::create_item_radiobox(wxString title, wxWindow* parent, wxString tooltip, int padding_left)
{
wxWindow *item = new wxWindow(parent, wxID_ANY, wxDefaultPosition, wxSize(-1, FromDIP(20)));
@ -341,6 +333,9 @@ void CameraPopup::check_func_supported(MachineObject *obj2)
m_obj = obj2;
if (m_obj == nullptr)
return;
update_custom_camera_switch();
// function supported
if (m_obj->has_ipcam) {
m_text_recording->Show();
@ -435,100 +430,15 @@ void CameraPopup::rescale()
m_panel->Layout();
main_sizer->Fit(m_panel);
SetClientSize(m_panel->GetSize());
PopupWindow::Update();
Layout();
}
void CameraPopup::OnLeftUp(wxMouseEvent &event)
void CameraPopup::on_dpi_changed(const wxRect& suggested_rect)
{
auto mouse_pos = ClientToScreen(event.GetPosition());
auto wxscroll_win_pos = m_panel->ClientToScreen(wxPoint(0, 0));
if (mouse_pos.x > wxscroll_win_pos.x && mouse_pos.y > wxscroll_win_pos.y && mouse_pos.x < (wxscroll_win_pos.x + m_panel->GetSize().x) && mouse_pos.y < (wxscroll_win_pos.y + m_panel->GetSize().y)) {
//recording
auto recording_rect = m_switch_recording->ClientToScreen(wxPoint(0, 0));
if (mouse_pos.x > recording_rect.x && mouse_pos.y > recording_rect.y && mouse_pos.x < (recording_rect.x + m_switch_recording->GetSize().x) && mouse_pos.y < (recording_rect.y + m_switch_recording->GetSize().y)) {
wxMouseEvent recording_evt(wxEVT_LEFT_DOWN);
m_switch_recording->GetEventHandler()->ProcessEvent(recording_evt);
return;
}
//vcamera
auto vcamera_rect = m_switch_vcamera->ClientToScreen(wxPoint(0, 0));
if (mouse_pos.x > vcamera_rect.x && mouse_pos.y > vcamera_rect.y && mouse_pos.x < (vcamera_rect.x + m_switch_vcamera->GetSize().x) && mouse_pos.y < (vcamera_rect.y + m_switch_vcamera->GetSize().y)) {
wxMouseEvent vcamera_evt(wxEVT_LEFT_DOWN);
m_switch_vcamera->GetEventHandler()->ProcessEvent(vcamera_evt);
return;
}
//resolution
for (int i = 0; i < (int)RESOLUTION_OPTIONS_NUM; ++i){
auto resolution_rbtn = resolution_rbtns[i];
auto rbtn_rect = resolution_rbtn->ClientToScreen(wxPoint(0, 0));
if (mouse_pos.x > rbtn_rect.x && mouse_pos.y > rbtn_rect.y && mouse_pos.x < (rbtn_rect.x + resolution_rbtn->GetSize().x) && mouse_pos.y < (rbtn_rect.y + resolution_rbtn->GetSize().y)) {
wxMouseEvent resolution_evt(wxEVT_LEFT_DOWN);
resolution_rbtn->GetEventHandler()->ProcessEvent(resolution_evt);
return;
}
auto resolution_txt = resolution_texts[i];
auto txt_rect = resolution_txt->ClientToScreen(wxPoint(0, 0));
if (mouse_pos.x > txt_rect.x && mouse_pos.y > txt_rect.y && mouse_pos.x < (txt_rect.x + resolution_txt->GetSize().x) && mouse_pos.y < (txt_rect.y + resolution_txt->GetSize().y)) {
wxMouseEvent resolution_evt(wxEVT_LEFT_DOWN);
resolution_txt->GetEventHandler()->ProcessEvent(resolution_evt);
return;
}
}
//hyper link
auto h_rect = vcamera_guide_link->ClientToScreen(wxPoint(0, 0));
if (mouse_pos.x > h_rect.x && mouse_pos.y > h_rect.y && mouse_pos.x < (h_rect.x + vcamera_guide_link->GetSize().x) && mouse_pos.y < (h_rect.y + vcamera_guide_link->GetSize().y)) {
auto url = wxString::Format(L"https://wiki.bambulab.com/%s/software/bambu-studio/virtual-camera", L"en");
wxLaunchDefaultBrowser(url);
}
}
}
void CameraPopup::start_interval()
{
m_interval_timer->Start(CAMERAPOPUP_CLICK_INTERVAL);
m_is_in_interval = true;
}
void CameraPopup::stop_interval(wxTimerEvent& event)
{
m_is_in_interval = false;
m_interval_timer->Stop();
}
void CameraPopup::OnDismiss() {
PopupWindow::OnDismiss();
this->start_interval();
}
bool CameraPopup::ProcessLeftDown(wxMouseEvent &event)
{
return PopupWindow::ProcessLeftDown(event);
}
bool CameraPopup::Show(bool show)
{
return PopupWindow::Show(show);
}
void CameraPopup::OnSize(wxSizeEvent &event)
{
event.Skip();
}
void CameraPopup::OnSetFocus(wxFocusEvent &event)
{
event.Skip();
}
void CameraPopup::OnKillFocus(wxFocusEvent &event)
{
event.Skip();
}
void CameraPopup::OnMouse(wxMouseEvent &event)
{
event.Skip();
m_panel->Layout();
main_sizer->Fit(m_panel);
SetClientSize(m_panel->GetSize());
Layout();
}
CameraItem::CameraItem(wxWindow *parent, std::string normal, std::string hover)

View file

@ -14,8 +14,9 @@
#include <wx/hyperlink.h>
#include "Widgets/SwitchButton.hpp"
#include "Widgets/RadioBox.hpp"
#include "Widgets/PopupWindow.hpp"
#include "Widgets/TextInput.hpp"
#include "GUI_Utils.hpp"
class Label;
namespace Slic3r {
namespace GUI {
@ -24,18 +25,12 @@ wxDECLARE_EVENT(EVT_VCAMERA_SWITCH, wxMouseEvent);
wxDECLARE_EVENT(EVT_SDCARD_ABSENT_HINT, wxCommandEvent);
wxDECLARE_EVENT(EVT_CAM_SOURCE_CHANGE, wxCommandEvent);
class CameraPopup : public PopupWindow
class CameraPopup : public DPIDialog
{
public:
CameraPopup(wxWindow *parent);
virtual ~CameraPopup() {}
// PopupWindow virtual methods are all overridden to log them
virtual void Popup(wxWindow *focus = NULL) wxOVERRIDE;
virtual void OnDismiss() wxOVERRIDE;
virtual bool ProcessLeftDown(wxMouseEvent &event) wxOVERRIDE;
virtual bool Show(bool show = true) wxOVERRIDE;
void sync_vcamera_state(bool show_vcamera);
void check_func_supported(MachineObject* obj);
void update(bool vcamera_streaming);
@ -50,12 +45,13 @@ public:
void rescale();
protected:
void on_dpi_changed(const wxRect& suggested_rect) override;
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);
void on_custom_camera_switch_toggled(wxCommandEvent& event);
void update_custom_camera_switch();
void on_manage_cameras_clicked(wxMouseEvent& event);
wxWindow * create_item_radiobox(wxString title, wxWindow *parent, wxString tooltip, int padding_left);
void select_curr_radiobox(int btn_idx);
@ -66,8 +62,6 @@ protected:
private:
MachineObject* m_obj { nullptr };
wxTimer* m_interval_timer{nullptr};
bool m_is_in_interval{ false };
wxStaticText* m_text_recording;
SwitchButton* m_switch_recording;
wxStaticText* m_text_vcamera;
@ -75,9 +69,8 @@ private:
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 };
SwitchButton* m_switch_custom_camera;
Label* m_manage_cameras_link{nullptr};
wxStaticText* m_text_resolution;
wxWindow* m_resolution_options[RESOLUTION_OPTIONS_NUM];
wxScrolledWindow *m_panel;
@ -89,18 +82,6 @@ private:
wxPanel* link_underline{ nullptr };
bool is_vcamera_show = false;
bool allow_alter_resolution = false;
void start_interval();
void stop_interval(wxTimerEvent& event);
void OnMouse(wxMouseEvent &event);
void OnSize(wxSizeEvent &event);
void OnSetFocus(wxFocusEvent &event);
void OnKillFocus(wxFocusEvent &event);
void OnLeftUp(wxMouseEvent& event);
private:
wxDECLARE_ABSTRACT_CLASS(CameraPopup);
wxDECLARE_EVENT_TABLE();
};

View file

@ -0,0 +1,177 @@
#include "wx/wxprec.h"
#ifndef WX_PRECOMP
#include "wx/wx.h"
#endif
#include "NativeMediaCtrl.h"
#include <boost/log/trivial.hpp>
#include <regex>
#include <algorithm>
wxDEFINE_EVENT(EVT_NATIVE_MEDIA_STATE_CHANGED, wxCommandEvent);
wxDEFINE_EVENT(EVT_NATIVE_MEDIA_ERROR, wxCommandEvent);
wxDEFINE_EVENT(EVT_NATIVE_MEDIA_SIZE_CHANGED, wxCommandEvent);
namespace Slic3r { namespace GUI {
StreamCredentials StreamCredentials::Parse(const std::string& url)
{
StreamCredentials creds;
std::regex url_regex(R"(^(\w+)://(?:([^:@]+)(?::([^@]*))?@)?([^/:]+)(?::(\d+))?(/.*)?$)");
std::smatch match;
if (std::regex_match(url, match, url_regex)) {
creds.scheme = match[1].str();
creds.username = match[2].str();
creds.password = match[3].str();
creds.host = match[4].str();
if (match[5].matched) {
creds.port = std::stoi(match[5].str());
}
creds.path = match[6].matched ? match[6].str() : "/";
}
return creds;
}
std::string StreamCredentials::BuildUrlWithoutCredentials() const
{
std::string result = scheme + "://" + host;
if (port > 0) {
result += ":" + std::to_string(port);
}
result += path;
return result;
}
StreamType NativeMediaCtrl::DetectStreamType(const wxString& url)
{
std::string url_lower = url.Lower().ToStdString();
if (url_lower.find("rtsp://") == 0) {
return StreamType::RTSP;
}
if (url_lower.find("rtsps://") == 0) {
return StreamType::RTSPS;
}
bool is_https = url_lower.find("https://") == 0;
bool is_http = url_lower.find("http://") == 0;
if (!is_http && !is_https) {
return StreamType::Unknown;
}
size_t path_start = url_lower.find('/', is_https ? 8 : 7);
std::string path = (path_start != std::string::npos) ? url_lower.substr(path_start) : "/";
size_t query_pos = path.find('?');
if (query_pos != std::string::npos) {
path = path.substr(0, query_pos);
}
if (path.length() >= 5) {
std::string ext = path.substr(path.length() - 5);
if (ext == ".mjpg" || (path.length() >= 6 && path.substr(path.length() - 6) == ".mjpeg")) {
return is_https ? StreamType::MJPEG_HTTPS : StreamType::MJPEG_HTTP;
}
}
if (path.find("/axis-cgi/mjpg") != std::string::npos ||
path.find("/cgi-bin/mjpg") != std::string::npos ||
path.find("/mjpg/") != std::string::npos ||
path.find("/mjpeg/") != std::string::npos) {
return is_https ? StreamType::MJPEG_HTTPS : StreamType::MJPEG_HTTP;
}
return is_https ? StreamType::HTTPS_VIDEO : StreamType::HTTP_VIDEO;
}
bool NativeMediaCtrl::IsSupported(const wxString& url)
{
StreamType type = DetectStreamType(url);
return type != StreamType::Unknown;
}
wxString NativeMediaCtrl::StreamTypeToString(StreamType type)
{
switch (type) {
case StreamType::RTSP: return "RTSP";
case StreamType::RTSPS: return "RTSPS (TLS)";
case StreamType::MJPEG_HTTP: return "MJPEG (HTTP)";
case StreamType::MJPEG_HTTPS: return "MJPEG (HTTPS)";
case StreamType::HTTP_VIDEO: return "HTTP Video";
case StreamType::HTTPS_VIDEO: return "HTTPS Video";
default: return "Unknown";
}
}
wxString NativeMediaCtrl::GetLastErrorMessage() const
{
NativeMediaError err = GetLastError();
switch (err) {
case NativeMediaError::None: return "";
case NativeMediaError::NetworkUnreachable: return "Network unreachable";
case NativeMediaError::AuthenticationFailed: return "Authentication failed";
case NativeMediaError::StreamNotFound: return "Stream not found";
case NativeMediaError::UnsupportedFormat: return "Unsupported format";
case NativeMediaError::DecoderError: return "Decoder error";
case NativeMediaError::ConnectionTimeout: return "Connection timeout";
case NativeMediaError::TLSError: return "TLS/SSL error";
case NativeMediaError::InternalError: return "Internal error";
default: return "Unknown error";
}
}
void NativeMediaCtrl::SetRetryEnabled(bool enabled)
{
m_retry_enabled = enabled;
if (!enabled) {
m_retry_timer.Stop();
}
}
bool NativeMediaCtrl::IsRetryEnabled() const
{
return m_retry_enabled;
}
void NativeMediaCtrl::OnRetryTimer(wxTimerEvent& event)
{
if (m_retry_count >= MAX_RETRIES) {
BOOST_LOG_TRIVIAL(warning) << "NativeMediaCtrl: Max retries reached, giving up";
wxCommandEvent error_event(EVT_NATIVE_MEDIA_ERROR);
error_event.SetEventObject(this);
error_event.SetInt(static_cast<int>(NativeMediaError::ConnectionTimeout));
wxPostEvent(this, error_event);
return;
}
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl: Retry attempt " << (m_retry_count + 1) << " of " << MAX_RETRIES;
m_retry_count++;
if (!m_current_url.empty()) {
Load(m_current_url);
Play();
}
}
void NativeMediaCtrl::ScheduleRetry()
{
if (!m_retry_enabled || m_retry_count >= MAX_RETRIES) {
return;
}
int delay = BASE_RETRY_DELAY_MS * (1 << std::min(m_retry_count, 4));
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl: Scheduling retry in " << delay << "ms";
m_retry_timer.StartOnce(delay);
}
void NativeMediaCtrl::ResetRetryState()
{
m_retry_count = 0;
m_retry_timer.Stop();
}
}} // namespace Slic3r::GUI

View file

@ -0,0 +1,106 @@
#ifndef NativeMediaCtrl_h
#define NativeMediaCtrl_h
#include <wx/window.h>
#include <wx/event.h>
#include <wx/timer.h>
#include <memory>
#include <string>
#include <functional>
wxDECLARE_EVENT(EVT_NATIVE_MEDIA_STATE_CHANGED, wxCommandEvent);
wxDECLARE_EVENT(EVT_NATIVE_MEDIA_ERROR, wxCommandEvent);
wxDECLARE_EVENT(EVT_NATIVE_MEDIA_SIZE_CHANGED, wxCommandEvent);
namespace Slic3r { namespace GUI {
enum class NativeMediaState {
Stopped,
Loading,
Playing,
Paused,
Error
};
enum class StreamType {
Unknown,
RTSP,
RTSPS,
MJPEG_HTTP,
MJPEG_HTTPS,
HTTP_VIDEO,
HTTPS_VIDEO
};
enum class NativeMediaError {
None = 0,
NetworkUnreachable = 1,
AuthenticationFailed = 2,
StreamNotFound = 3,
UnsupportedFormat = 4,
DecoderError = 5,
ConnectionTimeout = 6,
TLSError = 7,
InternalError = 99
};
struct StreamCredentials {
std::string username;
std::string password;
std::string host;
int port{0};
std::string path;
std::string scheme;
static StreamCredentials Parse(const std::string& url);
std::string BuildUrlWithoutCredentials() const;
bool HasCredentials() const { return !username.empty(); }
};
class NativeMediaCtrl : public wxWindow
{
public:
NativeMediaCtrl(wxWindow* parent);
~NativeMediaCtrl() override;
bool Load(const wxString& url);
void Play();
void Stop();
void Pause();
NativeMediaState GetState() const;
wxSize GetVideoSize() const;
NativeMediaError GetLastError() const;
wxString GetLastErrorMessage() const;
void SetRetryEnabled(bool enabled);
bool IsRetryEnabled() const;
static StreamType DetectStreamType(const wxString& url);
static bool IsSupported(const wxString& url);
static wxString StreamTypeToString(StreamType type);
protected:
void DoSetSize(int x, int y, int width, int height, int sizeFlags) override;
private:
class Impl;
std::unique_ptr<Impl> m_impl;
void OnRetryTimer(wxTimerEvent& event);
void ScheduleRetry();
void ResetRetryState();
wxTimer m_retry_timer;
int m_retry_count{0};
bool m_retry_enabled{true};
wxString m_current_url;
static constexpr int MAX_RETRIES = 5;
static constexpr int BASE_RETRY_DELAY_MS = 2000;
};
}} // namespace Slic3r::GUI
#endif // NativeMediaCtrl_h

View file

@ -0,0 +1,471 @@
#include "wx/wxprec.h"
#ifndef WX_PRECOMP
#include "wx/wx.h"
#endif
#include "NativeMediaCtrl.h"
#include <boost/log/trivial.hpp>
#ifdef __linux__
#include <gst/gst.h>
#include <gst/video/videooverlay.h>
#include <gtk/gtk.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif
#ifdef GDK_WINDOWING_WAYLAND
#include <gdk/gdkwayland.h>
#endif
namespace Slic3r { namespace GUI {
class NativeMediaCtrl::Impl
{
public:
Impl(NativeMediaCtrl* owner, GtkWidget* widget);
~Impl();
bool Load(const wxString& url);
void Play();
void Stop();
void Pause();
NativeMediaState GetState() const { return m_state; }
wxSize GetVideoSize() const { return m_video_size; }
NativeMediaError GetLastError() const { return m_error; }
void UpdateLayout(int width, int height);
private:
void SetupPipeline(const wxString& url);
void CleanupPipeline();
static GstBusSyncReply BusSyncHandler(GstBus* bus, GstMessage* message, gpointer user_data);
static gboolean BusCallback(GstBus* bus, GstMessage* message, gpointer user_data);
void HandleBusMessage(GstMessage* message);
void NotifyStateChanged();
NativeMediaError MapGstErrorToError(GError* error);
std::string BuildPipelineForRtsp(const std::string& url, const StreamCredentials& creds);
std::string BuildPipelineForMjpeg(const std::string& url, const StreamCredentials& creds);
NativeMediaCtrl* m_owner;
GtkWidget* m_widget;
GstElement* m_pipeline;
gulong m_bus_watch_id;
NativeMediaState m_state;
NativeMediaError m_error;
wxSize m_video_size;
wxString m_url;
guintptr m_window_handle;
};
NativeMediaCtrl::Impl::Impl(NativeMediaCtrl* owner, GtkWidget* widget)
: m_owner(owner)
, m_widget(widget)
, m_pipeline(nullptr)
, m_bus_watch_id(0)
, m_state(NativeMediaState::Stopped)
, m_error(NativeMediaError::None)
, m_video_size(1920, 1080)
, m_window_handle(0)
{
if (!gst_is_initialized()) {
gst_init(nullptr, nullptr);
}
GdkWindow* gdk_window = gtk_widget_get_window(m_widget);
if (gdk_window) {
#ifdef GDK_WINDOWING_X11
if (GDK_IS_X11_WINDOW(gdk_window)) {
m_window_handle = GDK_WINDOW_XID(gdk_window);
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl_Linux: Using X11 window handle";
}
#endif
#ifdef GDK_WINDOWING_WAYLAND
if (GDK_IS_WAYLAND_WINDOW(gdk_window)) {
struct wl_surface* wl_surface = gdk_wayland_window_get_wl_surface(gdk_window);
m_window_handle = reinterpret_cast<guintptr>(wl_surface);
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl_Linux: Using Wayland surface handle";
}
#endif
}
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl_Linux: GStreamer implementation initialized";
}
NativeMediaCtrl::Impl::~Impl()
{
CleanupPipeline();
}
void NativeMediaCtrl::Impl::CleanupPipeline()
{
if (m_bus_watch_id) {
g_source_remove(m_bus_watch_id);
m_bus_watch_id = 0;
}
if (m_pipeline) {
gst_element_set_state(m_pipeline, GST_STATE_NULL);
gst_object_unref(m_pipeline);
m_pipeline = nullptr;
}
}
bool NativeMediaCtrl::Impl::Load(const wxString& url)
{
CleanupPipeline();
m_url = url;
m_error = NativeMediaError::None;
m_state = NativeMediaState::Loading;
NotifyStateChanged();
SetupPipeline(url);
return m_pipeline != nullptr;
}
std::string NativeMediaCtrl::Impl::BuildPipelineForRtsp(const std::string& url, const StreamCredentials& creds)
{
std::string pipeline;
std::string clean_url = creds.HasCredentials() ? creds.BuildUrlWithoutCredentials() : url;
pipeline = "rtspsrc location=\"" + clean_url + "\" latency=100 buffer-mode=auto ";
pipeline += "protocols=tcp+udp-mcast+udp ";
pipeline += "tcp-timeout=5000000 ";
if (creds.HasCredentials()) {
pipeline += "user-id=\"" + creds.username + "\" ";
pipeline += "user-pw=\"" + creds.password + "\" ";
}
pipeline += "! decodebin ! videoconvert ! videoscale ! ";
pipeline += "video/x-raw,format=BGRx ! ";
pipeline += "autovideosink name=sink sync=false";
return pipeline;
}
std::string NativeMediaCtrl::Impl::BuildPipelineForMjpeg(const std::string& url, const StreamCredentials& creds)
{
std::string pipeline;
std::string clean_url = creds.HasCredentials() ? creds.BuildUrlWithoutCredentials() : url;
pipeline = "souphttpsrc location=\"" + clean_url + "\" ";
pipeline += "is-live=true do-timestamp=true ";
if (creds.HasCredentials()) {
pipeline += "user-id=\"" + creds.username + "\" ";
pipeline += "user-pw=\"" + creds.password + "\" ";
}
pipeline += "! multipartdemux ! jpegdec ! videoconvert ! ";
pipeline += "video/x-raw,format=BGRx ! ";
pipeline += "autovideosink name=sink sync=false";
return pipeline;
}
void NativeMediaCtrl::Impl::SetupPipeline(const wxString& url)
{
StreamType type = NativeMediaCtrl::DetectStreamType(url);
std::string url_str = url.ToStdString();
StreamCredentials creds = StreamCredentials::Parse(url_str);
std::string pipeline_desc;
if (type == StreamType::RTSP || type == StreamType::RTSPS) {
pipeline_desc = BuildPipelineForRtsp(url_str, creds);
} else if (type == StreamType::MJPEG_HTTP || type == StreamType::MJPEG_HTTPS) {
pipeline_desc = BuildPipelineForMjpeg(url_str, creds);
} else {
pipeline_desc = "playbin uri=\"" + url_str + "\"";
}
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl_Linux: Creating pipeline";
GError* error = nullptr;
m_pipeline = gst_parse_launch(pipeline_desc.c_str(), &error);
if (error) {
BOOST_LOG_TRIVIAL(error) << "NativeMediaCtrl_Linux: Pipeline creation failed: " << error->message;
m_error = NativeMediaError::InternalError;
g_error_free(error);
m_state = NativeMediaState::Error;
NotifyStateChanged();
return;
}
if (!m_pipeline) {
BOOST_LOG_TRIVIAL(error) << "NativeMediaCtrl_Linux: Pipeline is null";
m_error = NativeMediaError::InternalError;
m_state = NativeMediaState::Error;
NotifyStateChanged();
return;
}
GstBus* bus = gst_pipeline_get_bus(GST_PIPELINE(m_pipeline));
gst_bus_set_sync_handler(bus, BusSyncHandler, this, nullptr);
m_bus_watch_id = gst_bus_add_watch(bus, BusCallback, this);
gst_object_unref(bus);
GstElement* sink = gst_bin_get_by_name(GST_BIN(m_pipeline), "sink");
if (sink) {
if (GST_IS_VIDEO_OVERLAY(sink) && m_window_handle != 0) {
gst_video_overlay_set_window_handle(GST_VIDEO_OVERLAY(sink), m_window_handle);
}
gst_object_unref(sink);
}
}
GstBusSyncReply NativeMediaCtrl::Impl::BusSyncHandler(GstBus* bus, GstMessage* message, gpointer user_data)
{
if (GST_MESSAGE_TYPE(message) != GST_MESSAGE_ELEMENT)
return GST_BUS_PASS;
if (!gst_is_video_overlay_prepare_window_handle_message(message))
return GST_BUS_PASS;
Impl* self = static_cast<Impl*>(user_data);
if (self->m_window_handle != 0) {
gst_video_overlay_set_window_handle(GST_VIDEO_OVERLAY(GST_MESSAGE_SRC(message)), self->m_window_handle);
}
gst_message_unref(message);
return GST_BUS_DROP;
}
gboolean NativeMediaCtrl::Impl::BusCallback(GstBus* bus, GstMessage* message, gpointer user_data)
{
Impl* self = static_cast<Impl*>(user_data);
self->HandleBusMessage(message);
return TRUE;
}
NativeMediaError NativeMediaCtrl::Impl::MapGstErrorToError(GError* error)
{
if (g_error_matches(error, GST_RESOURCE_ERROR, GST_RESOURCE_ERROR_NOT_AUTHORIZED)) {
return NativeMediaError::AuthenticationFailed;
} else if (g_error_matches(error, GST_RESOURCE_ERROR, GST_RESOURCE_ERROR_NOT_FOUND)) {
return NativeMediaError::StreamNotFound;
} else if (g_error_matches(error, GST_RESOURCE_ERROR, GST_RESOURCE_ERROR_OPEN_READ) ||
g_error_matches(error, GST_RESOURCE_ERROR, GST_RESOURCE_ERROR_OPEN_READ_WRITE)) {
return NativeMediaError::NetworkUnreachable;
} else if (g_error_matches(error, GST_RESOURCE_ERROR, GST_RESOURCE_ERROR_BUSY)) {
return NativeMediaError::ConnectionTimeout;
} else if (g_error_matches(error, GST_STREAM_ERROR, GST_STREAM_ERROR_CODEC_NOT_FOUND) ||
g_error_matches(error, GST_STREAM_ERROR, GST_STREAM_ERROR_WRONG_TYPE)) {
return NativeMediaError::UnsupportedFormat;
} else if (g_error_matches(error, GST_STREAM_ERROR, GST_STREAM_ERROR_DECODE)) {
return NativeMediaError::DecoderError;
}
return NativeMediaError::InternalError;
}
void NativeMediaCtrl::Impl::HandleBusMessage(GstMessage* message)
{
switch (GST_MESSAGE_TYPE(message)) {
case GST_MESSAGE_ERROR: {
GError* err = nullptr;
gchar* debug = nullptr;
gst_message_parse_error(message, &err, &debug);
BOOST_LOG_TRIVIAL(error) << "NativeMediaCtrl_Linux: Error: " << err->message;
if (debug) {
BOOST_LOG_TRIVIAL(debug) << "NativeMediaCtrl_Linux: Debug: " << debug;
g_free(debug);
}
m_error = MapGstErrorToError(err);
g_error_free(err);
m_state = NativeMediaState::Error;
NotifyStateChanged();
wxCommandEvent event(EVT_NATIVE_MEDIA_ERROR);
event.SetEventObject(m_owner);
event.SetInt(static_cast<int>(m_error));
wxPostEvent(m_owner, event);
m_owner->ScheduleRetry();
break;
}
case GST_MESSAGE_EOS:
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl_Linux: End of stream";
m_state = NativeMediaState::Stopped;
NotifyStateChanged();
break;
case GST_MESSAGE_STATE_CHANGED:
if (GST_MESSAGE_SRC(message) == GST_OBJECT(m_pipeline)) {
GstState old_state, new_state;
gst_message_parse_state_changed(message, &old_state, &new_state, nullptr);
if (new_state == GST_STATE_PLAYING && m_state != NativeMediaState::Playing) {
m_state = NativeMediaState::Playing;
m_owner->ResetRetryState();
NotifyStateChanged();
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl_Linux: Now playing";
} else if (new_state == GST_STATE_PAUSED && m_state == NativeMediaState::Playing) {
m_state = NativeMediaState::Paused;
NotifyStateChanged();
}
}
break;
case GST_MESSAGE_STREAM_START:
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl_Linux: Stream started";
break;
case GST_MESSAGE_BUFFERING: {
gint percent = 0;
gst_message_parse_buffering(message, &percent);
BOOST_LOG_TRIVIAL(debug) << "NativeMediaCtrl_Linux: Buffering " << percent << "%";
break;
}
default:
break;
}
}
void NativeMediaCtrl::Impl::Play()
{
if (m_pipeline) {
GstStateChangeReturn ret = gst_element_set_state(m_pipeline, GST_STATE_PLAYING);
if (ret == GST_STATE_CHANGE_FAILURE) {
BOOST_LOG_TRIVIAL(error) << "NativeMediaCtrl_Linux: Failed to set pipeline to PLAYING";
m_error = NativeMediaError::InternalError;
m_state = NativeMediaState::Error;
NotifyStateChanged();
} else {
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl_Linux: Play started";
}
}
}
void NativeMediaCtrl::Impl::Stop()
{
if (m_pipeline) {
gst_element_set_state(m_pipeline, GST_STATE_NULL);
}
m_state = NativeMediaState::Stopped;
NotifyStateChanged();
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl_Linux: Stopped";
}
void NativeMediaCtrl::Impl::Pause()
{
if (m_pipeline) {
gst_element_set_state(m_pipeline, GST_STATE_PAUSED);
}
}
void NativeMediaCtrl::Impl::UpdateLayout(int width, int height)
{
if (m_pipeline) {
GstElement* sink = gst_bin_get_by_name(GST_BIN(m_pipeline), "sink");
if (sink) {
if (GST_IS_VIDEO_OVERLAY(sink)) {
gst_video_overlay_expose(GST_VIDEO_OVERLAY(sink));
}
gst_object_unref(sink);
}
}
}
void NativeMediaCtrl::Impl::NotifyStateChanged()
{
wxCommandEvent event(EVT_NATIVE_MEDIA_STATE_CHANGED);
event.SetEventObject(m_owner);
event.SetInt(static_cast<int>(m_state));
wxPostEvent(m_owner, event);
}
NativeMediaCtrl::NativeMediaCtrl(wxWindow* parent)
: wxWindow(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize)
, m_retry_enabled(true)
, m_retry_count(0)
{
SetBackgroundColour(*wxBLACK);
m_retry_timer.SetOwner(this);
Bind(wxEVT_TIMER, &NativeMediaCtrl::OnRetryTimer, this, m_retry_timer.GetId());
GtkWidget* widget = (GtkWidget*)GetHandle();
m_impl = std::make_unique<Impl>(this, widget);
}
NativeMediaCtrl::~NativeMediaCtrl()
{
m_retry_timer.Stop();
}
bool NativeMediaCtrl::Load(const wxString& url)
{
if (!IsSupported(url)) {
BOOST_LOG_TRIVIAL(warning) << "NativeMediaCtrl: Unsupported URL format: " << url.ToStdString();
return false;
}
m_current_url = url;
ResetRetryState();
StreamType type = DetectStreamType(url);
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl: Loading " << StreamTypeToString(type).ToStdString()
<< " stream: " << url.ToStdString();
return m_impl->Load(url);
}
void NativeMediaCtrl::Play()
{
if (m_impl) {
m_impl->Play();
}
}
void NativeMediaCtrl::Stop()
{
m_retry_timer.Stop();
ResetRetryState();
if (m_impl) {
m_impl->Stop();
}
}
void NativeMediaCtrl::Pause()
{
if (m_impl) {
m_impl->Pause();
}
}
NativeMediaState NativeMediaCtrl::GetState() const
{
return m_impl ? m_impl->GetState() : NativeMediaState::Stopped;
}
wxSize NativeMediaCtrl::GetVideoSize() const
{
return m_impl ? m_impl->GetVideoSize() : wxSize(1920, 1080);
}
NativeMediaError NativeMediaCtrl::GetLastError() const
{
return m_impl ? m_impl->GetLastError() : NativeMediaError::None;
}
void NativeMediaCtrl::DoSetSize(int x, int y, int width, int height, int sizeFlags)
{
wxWindow::DoSetSize(x, y, width, height, sizeFlags);
if (m_impl && width > 0 && height > 0) {
m_impl->UpdateLayout(width, height);
}
}
}} // namespace Slic3r::GUI
#endif // __linux__

View file

@ -0,0 +1,619 @@
#include "wx/wxprec.h"
#ifndef WX_PRECOMP
#include "wx/wx.h"
#endif
#import "NativeMediaCtrl.h"
#include <boost/log/trivial.hpp>
#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
#import <QuartzCore/QuartzCore.h>
#include <gst/gst.h>
#include <gst/app/gstappsink.h>
#include <gst/video/video.h>
namespace Slic3r { namespace GUI {
class NativeMediaCtrl::Impl
{
public:
Impl(NativeMediaCtrl* owner, NSView* view);
~Impl();
bool Load(const wxString& url);
void Play();
void Stop();
void Pause();
NativeMediaState GetState() const { return m_state; }
wxSize GetVideoSize() const { return m_video_size; }
NativeMediaError GetLastError() const { return m_error; }
void UpdateLayout(int width, int height);
private:
void SetupPipeline(const wxString& url);
void CleanupPipeline();
static GstFlowReturn OnNewSample(GstAppSink* sink, gpointer user_data);
static gboolean BusCallback(GstBus* bus, GstMessage* message, gpointer user_data);
void HandleBusMessage(GstMessage* message);
void HandleNewSample(GstSample* sample);
void NotifyStateChanged();
void NotifySizeChanged();
NativeMediaError MapGstErrorToError(GError* error);
std::string BuildPipelineForRtsp(const std::string& url, const StreamCredentials& creds);
std::string BuildPipelineForMjpeg(const std::string& url, const StreamCredentials& creds);
void UpdateStatusText(const std::string& text);
NativeMediaCtrl* m_owner;
NSView* m_view;
CALayer* m_image_layer;
CATextLayer* m_status_layer;
GstElement* m_pipeline;
GstElement* m_appsink;
gulong m_bus_watch_id;
NativeMediaState m_state;
NativeMediaError m_error;
wxSize m_video_size;
wxString m_url;
};
NativeMediaCtrl::Impl::Impl(NativeMediaCtrl* owner, NSView* view)
: m_owner(owner)
, m_view(view)
, m_image_layer(nil)
, m_status_layer(nil)
, m_pipeline(nullptr)
, m_appsink(nullptr)
, m_bus_watch_id(0)
, m_state(NativeMediaState::Stopped)
, m_error(NativeMediaError::None)
, m_video_size(1920, 1080)
{
if (!gst_is_initialized()) {
gst_init(nullptr, nullptr);
}
m_view.wantsLayer = YES;
m_view.layer = [[CALayer alloc] init];
m_view.layer.backgroundColor = [[NSColor blackColor] CGColor];
m_view.layer.masksToBounds = YES;
m_image_layer = [[CALayer alloc] init];
m_image_layer.contentsGravity = kCAGravityResizeAspect;
m_image_layer.backgroundColor = [[NSColor blackColor] CGColor];
m_image_layer.frame = m_view.layer.bounds;
m_image_layer.autoresizingMask = kCALayerWidthSizable | kCALayerHeightSizable;
[m_view.layer addSublayer:m_image_layer];
m_status_layer = [[CATextLayer alloc] init];
m_status_layer.alignmentMode = kCAAlignmentCenter;
m_status_layer.foregroundColor = [[NSColor whiteColor] CGColor];
m_status_layer.backgroundColor = [[NSColor colorWithWhite:0.0 alpha:0.7] CGColor];
m_status_layer.cornerRadius = 8.0;
m_status_layer.fontSize = 14.0;
m_status_layer.contentsScale = [[NSScreen mainScreen] backingScaleFactor];
m_status_layer.wrapped = YES;
m_status_layer.hidden = YES;
[m_view.layer addSublayer:m_status_layer];
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl_Mac: GStreamer implementation initialized";
}
NativeMediaCtrl::Impl::~Impl()
{
CleanupPipeline();
}
void NativeMediaCtrl::Impl::CleanupPipeline()
{
if (m_bus_watch_id) {
g_source_remove(m_bus_watch_id);
m_bus_watch_id = 0;
}
if (m_pipeline) {
gst_element_set_state(m_pipeline, GST_STATE_NULL);
gst_object_unref(m_pipeline);
m_pipeline = nullptr;
}
m_appsink = nullptr;
}
bool NativeMediaCtrl::Impl::Load(const wxString& url)
{
CleanupPipeline();
m_url = url;
m_error = NativeMediaError::None;
m_state = NativeMediaState::Loading;
UpdateStatusText("Connecting...");
NotifyStateChanged();
SetupPipeline(url);
return m_pipeline != nullptr;
}
std::string NativeMediaCtrl::Impl::BuildPipelineForRtsp(const std::string& url, const StreamCredentials& creds)
{
std::string pipeline;
std::string clean_url = creds.HasCredentials() ? creds.BuildUrlWithoutCredentials() : url;
pipeline = "rtspsrc location=\"" + clean_url + "\" latency=200 buffer-mode=auto ";
pipeline += "protocols=tcp+udp-mcast+udp ";
pipeline += "tcp-timeout=5000000 ";
if (creds.HasCredentials()) {
pipeline += "user-id=\"" + creds.username + "\" ";
pipeline += "user-pw=\"" + creds.password + "\" ";
}
pipeline += "! decodebin ! videoconvert ! videoscale ! ";
pipeline += "video/x-raw,format=BGRA ! ";
pipeline += "appsink name=sink emit-signals=true sync=false max-buffers=2 drop=true";
return pipeline;
}
std::string NativeMediaCtrl::Impl::BuildPipelineForMjpeg(const std::string& url, const StreamCredentials& creds)
{
std::string pipeline;
std::string clean_url = creds.HasCredentials() ? creds.BuildUrlWithoutCredentials() : url;
pipeline = "souphttpsrc location=\"" + clean_url + "\" ";
pipeline += "is-live=true do-timestamp=true timeout=10 ";
if (creds.HasCredentials()) {
pipeline += "user-id=\"" + creds.username + "\" ";
pipeline += "user-pw=\"" + creds.password + "\" ";
}
pipeline += "! multipartdemux ! jpegdec ! videoconvert ! ";
pipeline += "video/x-raw,format=BGRA ! ";
pipeline += "appsink name=sink emit-signals=true sync=false max-buffers=2 drop=true";
return pipeline;
}
void NativeMediaCtrl::Impl::SetupPipeline(const wxString& url)
{
StreamType type = NativeMediaCtrl::DetectStreamType(url);
std::string url_str = url.ToStdString();
StreamCredentials creds = StreamCredentials::Parse(url_str);
std::string pipeline_desc;
if (type == StreamType::RTSP || type == StreamType::RTSPS) {
pipeline_desc = BuildPipelineForRtsp(url_str, creds);
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl_Mac: Setting up RTSP pipeline";
} else if (type == StreamType::MJPEG_HTTP || type == StreamType::MJPEG_HTTPS) {
pipeline_desc = BuildPipelineForMjpeg(url_str, creds);
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl_Mac: Setting up MJPEG pipeline";
} else {
pipeline_desc = "playbin uri=\"" + url_str + "\" ! videoconvert ! video/x-raw,format=BGRA ! appsink name=sink emit-signals=true sync=false";
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl_Mac: Setting up generic pipeline";
}
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl_Mac: Pipeline: " << pipeline_desc;
GError* error = nullptr;
m_pipeline = gst_parse_launch(pipeline_desc.c_str(), &error);
if (error) {
BOOST_LOG_TRIVIAL(error) << "NativeMediaCtrl_Mac: Pipeline creation failed: " << error->message;
m_error = NativeMediaError::InternalError;
g_error_free(error);
m_state = NativeMediaState::Error;
NotifyStateChanged();
return;
}
if (!m_pipeline) {
BOOST_LOG_TRIVIAL(error) << "NativeMediaCtrl_Mac: Pipeline is null";
m_error = NativeMediaError::InternalError;
m_state = NativeMediaState::Error;
NotifyStateChanged();
return;
}
m_appsink = gst_bin_get_by_name(GST_BIN(m_pipeline), "sink");
if (m_appsink) {
GstAppSinkCallbacks callbacks = {nullptr, nullptr, OnNewSample, nullptr};
gst_app_sink_set_callbacks(GST_APP_SINK(m_appsink), &callbacks, this, nullptr);
gst_object_unref(m_appsink);
}
GstBus* bus = gst_pipeline_get_bus(GST_PIPELINE(m_pipeline));
m_bus_watch_id = gst_bus_add_watch(bus, BusCallback, this);
gst_object_unref(bus);
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl_Mac: Pipeline created successfully";
}
GstFlowReturn NativeMediaCtrl::Impl::OnNewSample(GstAppSink* sink, gpointer user_data)
{
Impl* impl = static_cast<Impl*>(user_data);
GstSample* sample = gst_app_sink_pull_sample(sink);
if (sample) {
impl->HandleNewSample(sample);
gst_sample_unref(sample);
}
return GST_FLOW_OK;
}
void NativeMediaCtrl::Impl::HandleNewSample(GstSample* sample)
{
GstCaps* caps = gst_sample_get_caps(sample);
if (!caps) return;
GstStructure* structure = gst_caps_get_structure(caps, 0);
int width = 0, height = 0;
gst_structure_get_int(structure, "width", &width);
gst_structure_get_int(structure, "height", &height);
if (width <= 0 || height <= 0) return;
bool size_changed = (m_video_size.GetWidth() != width || m_video_size.GetHeight() != height);
if (m_state == NativeMediaState::Loading) {
m_state = NativeMediaState::Playing;
m_video_size = wxSize(width, height);
m_owner->ResetRetryState();
UpdateStatusText("");
NotifyStateChanged();
NotifySizeChanged();
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl_Mac: Now playing " << width << "x" << height;
} else if (size_changed) {
m_video_size = wxSize(width, height);
NotifySizeChanged();
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl_Mac: Video size changed to " << width << "x" << height;
}
GstBuffer* buffer = gst_sample_get_buffer(sample);
if (!buffer) return;
GstMapInfo map;
if (!gst_buffer_map(buffer, &map, GST_MAP_READ)) return;
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate(
(void*)map.data,
width, height,
8,
width * 4,
colorSpace,
kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little
);
if (context) {
CGImageRef image = CGBitmapContextCreateImage(context);
if (image) {
CALayer* layer = [m_image_layer retain];
dispatch_async(dispatch_get_main_queue(), ^{
if (layer.superlayer != nil) {
[CATransaction begin];
[CATransaction setDisableActions:YES];
layer.contents = (__bridge id)image;
[CATransaction commit];
}
CGImageRelease(image);
[layer release];
});
}
CGContextRelease(context);
}
CGColorSpaceRelease(colorSpace);
gst_buffer_unmap(buffer, &map);
}
gboolean NativeMediaCtrl::Impl::BusCallback(GstBus* bus, GstMessage* message, gpointer user_data)
{
Impl* impl = static_cast<Impl*>(user_data);
impl->HandleBusMessage(message);
return TRUE;
}
NativeMediaError NativeMediaCtrl::Impl::MapGstErrorToError(GError* error)
{
if (g_error_matches(error, GST_RESOURCE_ERROR, GST_RESOURCE_ERROR_NOT_AUTHORIZED)) {
return NativeMediaError::AuthenticationFailed;
} else if (g_error_matches(error, GST_RESOURCE_ERROR, GST_RESOURCE_ERROR_NOT_FOUND)) {
return NativeMediaError::StreamNotFound;
} else if (g_error_matches(error, GST_RESOURCE_ERROR, GST_RESOURCE_ERROR_OPEN_READ) ||
g_error_matches(error, GST_RESOURCE_ERROR, GST_RESOURCE_ERROR_OPEN_READ_WRITE)) {
return NativeMediaError::NetworkUnreachable;
} else if (g_error_matches(error, GST_RESOURCE_ERROR, GST_RESOURCE_ERROR_BUSY)) {
return NativeMediaError::ConnectionTimeout;
} else if (g_error_matches(error, GST_STREAM_ERROR, GST_STREAM_ERROR_CODEC_NOT_FOUND) ||
g_error_matches(error, GST_STREAM_ERROR, GST_STREAM_ERROR_WRONG_TYPE)) {
return NativeMediaError::UnsupportedFormat;
} else if (g_error_matches(error, GST_STREAM_ERROR, GST_STREAM_ERROR_DECODE)) {
return NativeMediaError::DecoderError;
}
return NativeMediaError::InternalError;
}
void NativeMediaCtrl::Impl::HandleBusMessage(GstMessage* message)
{
switch (GST_MESSAGE_TYPE(message)) {
case GST_MESSAGE_ERROR: {
GError* err = nullptr;
gchar* debug = nullptr;
gst_message_parse_error(message, &err, &debug);
BOOST_LOG_TRIVIAL(error) << "NativeMediaCtrl_Mac: Error: " << err->message;
if (debug) {
BOOST_LOG_TRIVIAL(debug) << "NativeMediaCtrl_Mac: Debug: " << debug;
g_free(debug);
}
m_error = MapGstErrorToError(err);
g_error_free(err);
std::string error_msg;
switch (m_error) {
case NativeMediaError::NetworkUnreachable: error_msg = "Network unreachable"; break;
case NativeMediaError::AuthenticationFailed: error_msg = "Authentication failed"; break;
case NativeMediaError::StreamNotFound: error_msg = "Stream not found"; break;
case NativeMediaError::UnsupportedFormat: error_msg = "Unsupported format"; break;
case NativeMediaError::DecoderError: error_msg = "Decoder error"; break;
case NativeMediaError::ConnectionTimeout: error_msg = "Connection timeout"; break;
case NativeMediaError::TLSError: error_msg = "TLS/SSL error"; break;
default: error_msg = "Connection error"; break;
}
UpdateStatusText(error_msg + " - Retrying...");
m_state = NativeMediaState::Error;
NotifyStateChanged();
wxCommandEvent event(EVT_NATIVE_MEDIA_ERROR);
event.SetEventObject(m_owner);
event.SetInt(static_cast<int>(m_error));
wxPostEvent(m_owner, event);
m_owner->ScheduleRetry();
break;
}
case GST_MESSAGE_EOS:
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl_Mac: End of stream";
m_state = NativeMediaState::Stopped;
NotifyStateChanged();
break;
case GST_MESSAGE_STATE_CHANGED:
if (GST_MESSAGE_SRC(message) == GST_OBJECT(m_pipeline)) {
GstState old_state, new_state;
gst_message_parse_state_changed(message, &old_state, &new_state, nullptr);
BOOST_LOG_TRIVIAL(debug) << "NativeMediaCtrl_Mac: State changed from "
<< gst_element_state_get_name(old_state) << " to "
<< gst_element_state_get_name(new_state);
}
break;
case GST_MESSAGE_STREAM_START:
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl_Mac: Stream started";
break;
case GST_MESSAGE_BUFFERING: {
gint percent = 0;
gst_message_parse_buffering(message, &percent);
BOOST_LOG_TRIVIAL(debug) << "NativeMediaCtrl_Mac: Buffering " << percent << "%";
break;
}
default:
break;
}
}
void NativeMediaCtrl::Impl::Play()
{
if (m_pipeline) {
GstStateChangeReturn ret = gst_element_set_state(m_pipeline, GST_STATE_PLAYING);
if (ret == GST_STATE_CHANGE_FAILURE) {
BOOST_LOG_TRIVIAL(error) << "NativeMediaCtrl_Mac: Failed to set pipeline to PLAYING";
m_error = NativeMediaError::InternalError;
m_state = NativeMediaState::Error;
NotifyStateChanged();
} else {
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl_Mac: Play started";
}
}
}
void NativeMediaCtrl::Impl::Stop()
{
if (m_pipeline) {
gst_element_set_state(m_pipeline, GST_STATE_NULL);
}
m_state = NativeMediaState::Stopped;
NotifyStateChanged();
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl_Mac: Stopped";
}
void NativeMediaCtrl::Impl::Pause()
{
if (m_pipeline) {
gst_element_set_state(m_pipeline, GST_STATE_PAUSED);
}
}
void NativeMediaCtrl::Impl::UpdateLayout(int width, int height)
{
CALayer* imageLayer = [m_image_layer retain];
CATextLayer* statusLayer = [m_status_layer retain];
dispatch_async(dispatch_get_main_queue(), ^{
if (imageLayer.superlayer == nil || statusLayer.superlayer == nil) {
[imageLayer release];
[statusLayer release];
return;
}
[CATransaction begin];
[CATransaction setDisableActions:YES];
CGRect bounds = CGRectMake(0, 0, width, height);
imageLayer.frame = bounds;
if (!statusLayer.hidden) {
CGFloat labelWidth = fmin(width - 40, 300);
CGFloat labelHeight = 36;
CGFloat x = (width - labelWidth) / 2.0;
CGFloat y = (height - labelHeight) / 2.0;
statusLayer.frame = CGRectMake(x, y, labelWidth, labelHeight);
}
[CATransaction commit];
[imageLayer release];
[statusLayer release];
});
}
void NativeMediaCtrl::Impl::NotifyStateChanged()
{
wxCommandEvent event(EVT_NATIVE_MEDIA_STATE_CHANGED);
event.SetEventObject(m_owner);
event.SetInt(static_cast<int>(m_state));
wxPostEvent(m_owner, event);
}
void NativeMediaCtrl::Impl::NotifySizeChanged()
{
wxCommandEvent event(EVT_NATIVE_MEDIA_SIZE_CHANGED);
event.SetEventObject(m_owner);
wxPostEvent(m_owner, event);
}
void NativeMediaCtrl::Impl::UpdateStatusText(const std::string& text)
{
std::string captured_text = text;
CATextLayer* statusLayer = [m_status_layer retain];
NSView* view = [m_view retain];
dispatch_async(dispatch_get_main_queue(), ^{
if (statusLayer.superlayer == nil) {
[statusLayer release];
[view release];
return;
}
[CATransaction begin];
[CATransaction setDisableActions:YES];
if (captured_text.empty()) {
statusLayer.string = @"";
statusLayer.hidden = YES;
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl_Mac: Status text cleared";
} else {
NSString* nsText = [NSString stringWithUTF8String:captured_text.c_str()];
statusLayer.string = nsText;
statusLayer.hidden = NO;
CGRect bounds = view.layer.bounds;
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl_Mac: View bounds: " << bounds.size.width << "x" << bounds.size.height;
CGFloat labelWidth = 250;
CGFloat labelHeight = 36;
if (bounds.size.width > 40 && bounds.size.height > 40) {
labelWidth = fmin(bounds.size.width - 40, 300);
CGFloat x = (bounds.size.width - labelWidth) / 2.0;
CGFloat y = (bounds.size.height - labelHeight) / 2.0;
statusLayer.frame = CGRectMake(x, y, labelWidth, labelHeight);
} else {
statusLayer.frame = CGRectMake(20, 20, labelWidth, labelHeight);
}
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl_Mac: Status text set to '" << captured_text << "'";
}
[CATransaction commit];
[statusLayer release];
[view release];
});
}
NativeMediaCtrl::NativeMediaCtrl(wxWindow* parent)
: wxWindow(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize)
, m_retry_enabled(true)
, m_retry_count(0)
{
SetBackgroundColour(*wxBLACK);
m_retry_timer.SetOwner(this);
Bind(wxEVT_TIMER, &NativeMediaCtrl::OnRetryTimer, this, m_retry_timer.GetId());
NSView* view = (NSView*)GetHandle();
m_impl = std::make_unique<Impl>(this, view);
}
NativeMediaCtrl::~NativeMediaCtrl()
{
m_retry_timer.Stop();
}
bool NativeMediaCtrl::Load(const wxString& url)
{
if (!IsSupported(url)) {
BOOST_LOG_TRIVIAL(warning) << "NativeMediaCtrl: Unsupported URL format: " << url.ToStdString();
return false;
}
m_current_url = url;
ResetRetryState();
StreamType type = DetectStreamType(url);
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl: Loading " << StreamTypeToString(type).ToStdString()
<< " stream: " << url.ToStdString();
return m_impl->Load(url);
}
void NativeMediaCtrl::Play()
{
if (m_impl) {
m_impl->Play();
}
}
void NativeMediaCtrl::Stop()
{
m_retry_timer.Stop();
ResetRetryState();
if (m_impl) {
m_impl->Stop();
}
}
void NativeMediaCtrl::Pause()
{
if (m_impl) {
m_impl->Pause();
}
}
NativeMediaState NativeMediaCtrl::GetState() const
{
return m_impl ? m_impl->GetState() : NativeMediaState::Stopped;
}
wxSize NativeMediaCtrl::GetVideoSize() const
{
return m_impl ? m_impl->GetVideoSize() : wxSize(1920, 1080);
}
NativeMediaError NativeMediaCtrl::GetLastError() const
{
return m_impl ? m_impl->GetLastError() : NativeMediaError::None;
}
void NativeMediaCtrl::DoSetSize(int x, int y, int width, int height, int sizeFlags)
{
wxWindow::DoSetSize(x, y, width, height, sizeFlags);
if (m_impl && width > 0 && height > 0) {
m_impl->UpdateLayout(width, height);
}
}
}} // namespace Slic3r::GUI

View file

@ -0,0 +1,876 @@
#include "wx/wxprec.h"
#ifndef WX_PRECOMP
#include "wx/wx.h"
#endif
#include "NativeMediaCtrl.h"
#include <boost/log/trivial.hpp>
#ifdef _WIN32
#include <windows.h>
#include <mfapi.h>
#include <mfidl.h>
#include <mfreadwrite.h>
#include <mferror.h>
#include <d3d11.h>
#include <dxgi1_2.h>
#include <d3dcompiler.h>
#include <wrl/client.h>
#include <string>
#include <atomic>
#include <thread>
#include <mutex>
#pragma comment(lib, "mfplat.lib")
#pragma comment(lib, "mf.lib")
#pragma comment(lib, "mfreadwrite.lib")
#pragma comment(lib, "mfuuid.lib")
#pragma comment(lib, "d3d11.lib")
#pragma comment(lib, "dxgi.lib")
#pragma comment(lib, "d3dcompiler.lib")
using Microsoft::WRL::ComPtr;
namespace Slic3r { namespace GUI {
class NativeMediaCtrl::Impl
{
public:
Impl(NativeMediaCtrl* owner, HWND hwnd);
~Impl();
bool Load(const wxString& url);
void Play();
void Stop();
void Pause();
NativeMediaState GetState() const { return m_state.load(); }
wxSize GetVideoSize() const { return m_video_size; }
NativeMediaError GetLastError() const { return m_error; }
void UpdateLayout(int width, int height);
private:
bool InitializeMediaFoundation();
void ShutdownMediaFoundation();
bool CreateMediaSource(const wxString& url);
bool CreateSourceReader();
bool CreateRenderingResources();
bool CreateVideoTexture(UINT width, UINT height, DXGI_FORMAT format);
void RenderVideoFrame(const BYTE* data, DWORD length);
void RenderLoop();
void CleanupResources();
void NotifyStateChanged(NativeMediaState state);
void NotifyError(NativeMediaError error);
NativeMediaError MapHResultToError(HRESULT hr);
NativeMediaCtrl* m_owner;
HWND m_hwnd;
std::atomic<NativeMediaState> m_state;
NativeMediaError m_error;
wxSize m_video_size;
wxString m_url;
ComPtr<IMFMediaSource> m_media_source;
ComPtr<IMFSourceReader> m_source_reader;
ComPtr<ID3D11Device> m_d3d_device;
ComPtr<ID3D11DeviceContext> m_d3d_context;
ComPtr<IDXGISwapChain1> m_swap_chain;
ComPtr<ID3D11RenderTargetView> m_render_target;
ComPtr<ID3D11Texture2D> m_video_texture;
ComPtr<ID3D11ShaderResourceView> m_video_srv;
ComPtr<ID3D11VertexShader> m_vertex_shader;
ComPtr<ID3D11PixelShader> m_pixel_shader;
ComPtr<ID3D11Buffer> m_vertex_buffer;
ComPtr<ID3D11SamplerState> m_sampler_state;
ComPtr<ID3D11InputLayout> m_input_layout;
std::thread m_render_thread;
std::atomic<bool> m_running;
std::atomic<bool> m_paused;
std::mutex m_mutex;
bool m_mf_initialized;
int m_width;
int m_height;
int m_video_width;
int m_video_height;
GUID m_video_format;
};
NativeMediaCtrl::Impl::Impl(NativeMediaCtrl* owner, HWND hwnd)
: m_owner(owner)
, m_hwnd(hwnd)
, m_state(NativeMediaState::Stopped)
, m_error(NativeMediaError::None)
, m_video_size(1920, 1080)
, m_running(false)
, m_paused(false)
, m_mf_initialized(false)
, m_width(640)
, m_height(480)
, m_video_width(0)
, m_video_height(0)
, m_video_format(GUID_NULL)
{
m_mf_initialized = InitializeMediaFoundation();
if (m_mf_initialized) {
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl_Win: Media Foundation initialized";
} else {
BOOST_LOG_TRIVIAL(error) << "NativeMediaCtrl_Win: Failed to initialize Media Foundation";
}
}
NativeMediaCtrl::Impl::~Impl()
{
Stop();
ShutdownMediaFoundation();
}
bool NativeMediaCtrl::Impl::InitializeMediaFoundation()
{
HRESULT hr = MFStartup(MF_VERSION);
if (FAILED(hr)) {
BOOST_LOG_TRIVIAL(error) << "NativeMediaCtrl_Win: MFStartup failed: " << std::hex << hr;
return false;
}
D3D_FEATURE_LEVEL feature_levels[] = {
D3D_FEATURE_LEVEL_11_1,
D3D_FEATURE_LEVEL_11_0,
D3D_FEATURE_LEVEL_10_1,
D3D_FEATURE_LEVEL_10_0
};
UINT flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT | D3D11_CREATE_DEVICE_VIDEO_SUPPORT;
#ifdef _DEBUG
flags |= D3D11_CREATE_DEVICE_DEBUG;
#endif
hr = D3D11CreateDevice(
nullptr,
D3D_DRIVER_TYPE_HARDWARE,
nullptr,
flags,
feature_levels,
ARRAYSIZE(feature_levels),
D3D11_SDK_VERSION,
&m_d3d_device,
nullptr,
&m_d3d_context
);
if (FAILED(hr)) {
hr = D3D11CreateDevice(
nullptr,
D3D_DRIVER_TYPE_WARP,
nullptr,
flags,
feature_levels,
ARRAYSIZE(feature_levels),
D3D11_SDK_VERSION,
&m_d3d_device,
nullptr,
&m_d3d_context
);
}
if (FAILED(hr)) {
BOOST_LOG_TRIVIAL(error) << "NativeMediaCtrl_Win: D3D11CreateDevice failed: " << std::hex << hr;
return false;
}
ComPtr<IDXGIDevice1> dxgi_device;
hr = m_d3d_device.As(&dxgi_device);
if (FAILED(hr)) return false;
ComPtr<IDXGIAdapter> adapter;
hr = dxgi_device->GetAdapter(&adapter);
if (FAILED(hr)) return false;
ComPtr<IDXGIFactory2> factory;
hr = adapter->GetParent(IID_PPV_ARGS(&factory));
if (FAILED(hr)) return false;
RECT rect;
GetClientRect(m_hwnd, &rect);
m_width = std::max(1L, rect.right - rect.left);
m_height = std::max(1L, rect.bottom - rect.top);
DXGI_SWAP_CHAIN_DESC1 swap_chain_desc = {};
swap_chain_desc.Width = m_width;
swap_chain_desc.Height = m_height;
swap_chain_desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
swap_chain_desc.SampleDesc.Count = 1;
swap_chain_desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
swap_chain_desc.BufferCount = 2;
swap_chain_desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL;
hr = factory->CreateSwapChainForHwnd(
m_d3d_device.Get(),
m_hwnd,
&swap_chain_desc,
nullptr,
nullptr,
&m_swap_chain
);
if (FAILED(hr)) {
BOOST_LOG_TRIVIAL(error) << "NativeMediaCtrl_Win: CreateSwapChain failed: " << std::hex << hr;
return false;
}
ComPtr<ID3D11Texture2D> back_buffer;
hr = m_swap_chain->GetBuffer(0, IID_PPV_ARGS(&back_buffer));
if (FAILED(hr)) return false;
hr = m_d3d_device->CreateRenderTargetView(back_buffer.Get(), nullptr, &m_render_target);
if (FAILED(hr)) return false;
if (!CreateRenderingResources()) {
BOOST_LOG_TRIVIAL(error) << "NativeMediaCtrl_Win: Failed to create rendering resources";
return false;
}
return true;
}
bool NativeMediaCtrl::Impl::CreateRenderingResources()
{
const char* vertex_shader_code = R"(
struct VS_OUTPUT {
float4 position : SV_POSITION;
float2 texcoord : TEXCOORD0;
};
VS_OUTPUT main(uint id : SV_VertexID) {
VS_OUTPUT output;
float2 positions[4] = {
float2(-1.0, -1.0),
float2(-1.0, 1.0),
float2( 1.0, -1.0),
float2( 1.0, 1.0)
};
float2 texcoords[4] = {
float2(0.0, 1.0),
float2(0.0, 0.0),
float2(1.0, 1.0),
float2(1.0, 0.0)
};
output.position = float4(positions[id], 0.0, 1.0);
output.texcoord = texcoords[id];
return output;
}
)";
const char* pixel_shader_code = R"(
Texture2D videoTexture : register(t0);
SamplerState videoSampler : register(s0);
struct PS_INPUT {
float4 position : SV_POSITION;
float2 texcoord : TEXCOORD0;
};
float4 main(PS_INPUT input) : SV_TARGET {
float4 color = videoTexture.Sample(videoSampler, input.texcoord);
return float4(color.bgr, color.a);
}
)";
ComPtr<ID3DBlob> vs_blob;
ComPtr<ID3DBlob> error_blob;
HRESULT hr = D3DCompile(
vertex_shader_code,
strlen(vertex_shader_code),
nullptr,
nullptr,
nullptr,
"main",
"vs_5_0",
0,
0,
&vs_blob,
&error_blob
);
if (FAILED(hr)) {
if (error_blob) {
BOOST_LOG_TRIVIAL(error) << "NativeMediaCtrl_Win: Vertex shader compilation failed: "
<< (char*)error_blob->GetBufferPointer();
}
return false;
}
hr = m_d3d_device->CreateVertexShader(
vs_blob->GetBufferPointer(),
vs_blob->GetBufferSize(),
nullptr,
&m_vertex_shader
);
if (FAILED(hr)) return false;
ComPtr<ID3DBlob> ps_blob;
hr = D3DCompile(
pixel_shader_code,
strlen(pixel_shader_code),
nullptr,
nullptr,
nullptr,
"main",
"ps_5_0",
0,
0,
&ps_blob,
&error_blob
);
if (FAILED(hr)) {
if (error_blob) {
BOOST_LOG_TRIVIAL(error) << "NativeMediaCtrl_Win: Pixel shader compilation failed: "
<< (char*)error_blob->GetBufferPointer();
}
return false;
}
hr = m_d3d_device->CreatePixelShader(
ps_blob->GetBufferPointer(),
ps_blob->GetBufferSize(),
nullptr,
&m_pixel_shader
);
if (FAILED(hr)) return false;
D3D11_SAMPLER_DESC sampler_desc = {};
sampler_desc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
sampler_desc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP;
sampler_desc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP;
sampler_desc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP;
sampler_desc.ComparisonFunc = D3D11_COMPARISON_NEVER;
sampler_desc.MinLOD = 0;
sampler_desc.MaxLOD = D3D11_FLOAT32_MAX;
hr = m_d3d_device->CreateSamplerState(&sampler_desc, &m_sampler_state);
if (FAILED(hr)) return false;
return true;
}
bool NativeMediaCtrl::Impl::CreateVideoTexture(UINT width, UINT height, DXGI_FORMAT format)
{
m_video_texture.Reset();
m_video_srv.Reset();
D3D11_TEXTURE2D_DESC texture_desc = {};
texture_desc.Width = width;
texture_desc.Height = height;
texture_desc.MipLevels = 1;
texture_desc.ArraySize = 1;
texture_desc.Format = format;
texture_desc.SampleDesc.Count = 1;
texture_desc.Usage = D3D11_USAGE_DYNAMIC;
texture_desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
texture_desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
HRESULT hr = m_d3d_device->CreateTexture2D(&texture_desc, nullptr, &m_video_texture);
if (FAILED(hr)) {
BOOST_LOG_TRIVIAL(error) << "NativeMediaCtrl_Win: Failed to create video texture: " << std::hex << hr;
return false;
}
D3D11_SHADER_RESOURCE_VIEW_DESC srv_desc = {};
srv_desc.Format = format;
srv_desc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
srv_desc.Texture2D.MipLevels = 1;
hr = m_d3d_device->CreateShaderResourceView(m_video_texture.Get(), &srv_desc, &m_video_srv);
if (FAILED(hr)) {
BOOST_LOG_TRIVIAL(error) << "NativeMediaCtrl_Win: Failed to create shader resource view: " << std::hex << hr;
return false;
}
m_video_width = width;
m_video_height = height;
return true;
}
void NativeMediaCtrl::Impl::RenderVideoFrame(const BYTE* data, DWORD length)
{
if (!m_video_texture || !m_video_srv || !m_d3d_context || !m_render_target) {
return;
}
D3D11_MAPPED_SUBRESOURCE mapped;
HRESULT hr = m_d3d_context->Map(m_video_texture.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped);
if (FAILED(hr)) {
BOOST_LOG_TRIVIAL(error) << "NativeMediaCtrl_Win: Failed to map video texture: " << std::hex << hr;
return;
}
UINT bytes_per_pixel = 4;
UINT row_bytes = m_video_width * bytes_per_pixel;
if (mapped.RowPitch == row_bytes) {
memcpy(mapped.pData, data, std::min<DWORD>(length, row_bytes * m_video_height));
} else {
const BYTE* src = data;
BYTE* dst = static_cast<BYTE*>(mapped.pData);
for (UINT y = 0; y < m_video_height && src < data + length; ++y) {
memcpy(dst, src, row_bytes);
src += row_bytes;
dst += mapped.RowPitch;
}
}
m_d3d_context->Unmap(m_video_texture.Get(), 0);
float clear_color[] = { 0.0f, 0.0f, 0.0f, 1.0f };
m_d3d_context->ClearRenderTargetView(m_render_target.Get(), clear_color);
D3D11_VIEWPORT viewport = {};
viewport.Width = static_cast<float>(m_width);
viewport.Height = static_cast<float>(m_height);
viewport.MinDepth = 0.0f;
viewport.MaxDepth = 1.0f;
m_d3d_context->RSSetViewports(1, &viewport);
m_d3d_context->OMSetRenderTargets(1, m_render_target.GetAddressOf(), nullptr);
m_d3d_context->VSSetShader(m_vertex_shader.Get(), nullptr, 0);
m_d3d_context->PSSetShader(m_pixel_shader.Get(), nullptr, 0);
m_d3d_context->PSSetShaderResources(0, 1, m_video_srv.GetAddressOf());
m_d3d_context->PSSetSamplers(0, 1, m_sampler_state.GetAddressOf());
m_d3d_context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
m_d3d_context->Draw(4, 0);
m_swap_chain->Present(1, 0);
}
void NativeMediaCtrl::Impl::ShutdownMediaFoundation()
{
CleanupResources();
m_video_srv.Reset();
m_video_texture.Reset();
m_sampler_state.Reset();
m_input_layout.Reset();
m_pixel_shader.Reset();
m_vertex_shader.Reset();
m_vertex_buffer.Reset();
m_render_target.Reset();
m_swap_chain.Reset();
m_d3d_context.Reset();
m_d3d_device.Reset();
if (m_mf_initialized) {
MFShutdown();
m_mf_initialized = false;
}
}
void NativeMediaCtrl::Impl::CleanupResources()
{
m_running = false;
if (m_render_thread.joinable()) {
m_render_thread.join();
}
std::lock_guard<std::mutex> lock(m_mutex);
m_source_reader.Reset();
m_media_source.Reset();
}
bool NativeMediaCtrl::Impl::Load(const wxString& url)
{
CleanupResources();
m_url = url;
m_error = NativeMediaError::None;
NotifyStateChanged(NativeMediaState::Loading);
if (!m_mf_initialized) {
BOOST_LOG_TRIVIAL(error) << "NativeMediaCtrl_Win: Media Foundation not initialized";
m_error = NativeMediaError::InternalError;
NotifyStateChanged(NativeMediaState::Error);
return false;
}
if (!CreateMediaSource(url)) {
NotifyError(m_error);
return false;
}
if (!CreateSourceReader()) {
NotifyError(m_error);
return false;
}
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl_Win: Loaded URL: " << url.ToStdString();
return true;
}
bool NativeMediaCtrl::Impl::CreateMediaSource(const wxString& url)
{
std::wstring wide_url = url.ToStdWstring();
ComPtr<IMFSourceResolver> resolver;
HRESULT hr = MFCreateSourceResolver(&resolver);
if (FAILED(hr)) {
BOOST_LOG_TRIVIAL(error) << "NativeMediaCtrl_Win: MFCreateSourceResolver failed: " << std::hex << hr;
m_error = NativeMediaError::InternalError;
return false;
}
MF_OBJECT_TYPE object_type = MF_OBJECT_INVALID;
ComPtr<IUnknown> source;
hr = resolver->CreateObjectFromURL(
wide_url.c_str(),
MF_RESOLUTION_MEDIASOURCE | MF_RESOLUTION_READ,
nullptr,
&object_type,
&source
);
if (FAILED(hr)) {
BOOST_LOG_TRIVIAL(error) << "NativeMediaCtrl_Win: CreateObjectFromURL failed: " << std::hex << hr;
m_error = MapHResultToError(hr);
return false;
}
hr = source.As(&m_media_source);
if (FAILED(hr)) {
m_error = NativeMediaError::InternalError;
return false;
}
return true;
}
bool NativeMediaCtrl::Impl::CreateSourceReader()
{
ComPtr<IMFAttributes> attributes;
HRESULT hr = MFCreateAttributes(&attributes, 3);
if (FAILED(hr)) return false;
hr = attributes->SetUINT32(MF_READWRITE_ENABLE_HARDWARE_TRANSFORMS, TRUE);
if (FAILED(hr)) return false;
hr = attributes->SetUINT32(MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING, TRUE);
if (FAILED(hr)) return false;
if (m_d3d_device) {
ComPtr<IMFDXGIDeviceManager> dxgi_manager;
UINT reset_token = 0;
hr = MFCreateDXGIDeviceManager(&reset_token, &dxgi_manager);
if (SUCCEEDED(hr)) {
hr = dxgi_manager->ResetDevice(m_d3d_device.Get(), reset_token);
if (SUCCEEDED(hr)) {
attributes->SetUnknown(MF_SOURCE_READER_D3D_MANAGER, dxgi_manager.Get());
}
}
}
hr = MFCreateSourceReaderFromMediaSource(m_media_source.Get(), attributes.Get(), &m_source_reader);
if (FAILED(hr)) {
BOOST_LOG_TRIVIAL(error) << "NativeMediaCtrl_Win: MFCreateSourceReaderFromMediaSource failed: " << std::hex << hr;
m_error = NativeMediaError::InternalError;
return false;
}
ComPtr<IMFMediaType> video_type;
hr = MFCreateMediaType(&video_type);
if (FAILED(hr)) return false;
video_type->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video);
video_type->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_RGB32);
hr = m_source_reader->SetCurrentMediaType(MF_SOURCE_READER_FIRST_VIDEO_STREAM, nullptr, video_type.Get());
if (FAILED(hr)) {
BOOST_LOG_TRIVIAL(error) << "NativeMediaCtrl_Win: SetCurrentMediaType failed for RGB32: " << std::hex << hr;
m_error = NativeMediaError::UnsupportedFormat;
return false;
}
ComPtr<IMFMediaType> actual_type;
hr = m_source_reader->GetCurrentMediaType(MF_SOURCE_READER_FIRST_VIDEO_STREAM, &actual_type);
if (SUCCEEDED(hr)) {
UINT32 width = 0, height = 0;
MFGetAttributeSize(actual_type.Get(), MF_MT_FRAME_SIZE, &width, &height);
if (width > 0 && height > 0) {
m_video_size = wxSize(width, height);
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl_Win: Video size: " << width << "x" << height;
GUID subtype = GUID_NULL;
actual_type->GetGUID(MF_MT_SUBTYPE, &subtype);
m_video_format = subtype;
if (!CreateVideoTexture(width, height, DXGI_FORMAT_B8G8R8A8_UNORM)) {
BOOST_LOG_TRIVIAL(error) << "NativeMediaCtrl_Win: Failed to create video texture";
return false;
}
}
}
return true;
}
void NativeMediaCtrl::Impl::Play()
{
if (!m_source_reader) {
BOOST_LOG_TRIVIAL(warning) << "NativeMediaCtrl_Win: Cannot play - no source reader";
return;
}
m_paused = false;
if (m_running) {
NotifyStateChanged(NativeMediaState::Playing);
return;
}
m_running = true;
m_render_thread = std::thread(&Impl::RenderLoop, this);
NotifyStateChanged(NativeMediaState::Playing);
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl_Win: Playback started";
}
void NativeMediaCtrl::Impl::Stop()
{
m_running = false;
m_paused = false;
if (m_render_thread.joinable()) {
m_render_thread.join();
}
NotifyStateChanged(NativeMediaState::Stopped);
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl_Win: Playback stopped";
}
void NativeMediaCtrl::Impl::Pause()
{
m_paused = true;
NotifyStateChanged(NativeMediaState::Paused);
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl_Win: Playback paused";
}
void NativeMediaCtrl::Impl::RenderLoop()
{
while (m_running) {
if (m_paused) {
std::this_thread::sleep_for(std::chrono::milliseconds(16));
continue;
}
ComPtr<IMFSample> sample;
DWORD flags = 0;
LONGLONG timestamp = 0;
HRESULT hr;
{
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_source_reader) break;
hr = m_source_reader->ReadSample(
MF_SOURCE_READER_FIRST_VIDEO_STREAM,
0,
nullptr,
&flags,
&timestamp,
&sample
);
}
if (FAILED(hr)) {
BOOST_LOG_TRIVIAL(error) << "NativeMediaCtrl_Win: ReadSample failed: " << std::hex << hr;
m_error = MapHResultToError(hr);
NotifyError(m_error);
m_owner->ScheduleRetry();
break;
}
if (flags & MF_SOURCE_READERF_ENDOFSTREAM) {
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl_Win: End of stream";
NotifyStateChanged(NativeMediaState::Stopped);
break;
}
if (flags & MF_SOURCE_READERF_ERROR) {
BOOST_LOG_TRIVIAL(error) << "NativeMediaCtrl_Win: Stream error";
m_error = NativeMediaError::DecoderError;
NotifyError(m_error);
m_owner->ScheduleRetry();
break;
}
if (sample) {
ComPtr<IMFMediaBuffer> buffer;
hr = sample->GetBufferByIndex(0, &buffer);
if (SUCCEEDED(hr)) {
BYTE* data = nullptr;
DWORD length = 0;
hr = buffer->Lock(&data, nullptr, &length);
if (SUCCEEDED(hr) && data) {
RenderVideoFrame(data, length);
buffer->Unlock();
}
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
void NativeMediaCtrl::Impl::UpdateLayout(int width, int height)
{
if (width <= 0 || height <= 0) return;
std::lock_guard<std::mutex> lock(m_mutex);
m_width = width;
m_height = height;
if (m_swap_chain && m_d3d_device) {
m_render_target.Reset();
HRESULT hr = m_swap_chain->ResizeBuffers(2, width, height, DXGI_FORMAT_B8G8R8A8_UNORM, 0);
if (SUCCEEDED(hr)) {
ComPtr<ID3D11Texture2D> back_buffer;
hr = m_swap_chain->GetBuffer(0, IID_PPV_ARGS(&back_buffer));
if (SUCCEEDED(hr)) {
m_d3d_device->CreateRenderTargetView(back_buffer.Get(), nullptr, &m_render_target);
}
}
}
}
NativeMediaError NativeMediaCtrl::Impl::MapHResultToError(HRESULT hr)
{
switch (hr) {
case MF_E_NET_NOCONNECTION:
case E_ACCESSDENIED:
return NativeMediaError::NetworkUnreachable;
case MF_E_NET_TIMEOUT:
return NativeMediaError::ConnectionTimeout;
case MF_E_UNSUPPORTED_FORMAT:
case MF_E_INVALIDMEDIATYPE:
return NativeMediaError::UnsupportedFormat;
case MF_E_SOURCERESOLVER_MUTUALLY_EXCLUSIVE_FLAGS:
case MF_E_UNSUPPORTED_SCHEME:
return NativeMediaError::StreamNotFound;
default:
return NativeMediaError::InternalError;
}
}
void NativeMediaCtrl::Impl::NotifyStateChanged(NativeMediaState state)
{
m_state = state;
wxCommandEvent event(EVT_NATIVE_MEDIA_STATE_CHANGED);
event.SetEventObject(m_owner);
event.SetInt(static_cast<int>(state));
wxPostEvent(m_owner, event);
}
void NativeMediaCtrl::Impl::NotifyError(NativeMediaError error)
{
m_state = NativeMediaState::Error;
wxCommandEvent event(EVT_NATIVE_MEDIA_ERROR);
event.SetEventObject(m_owner);
event.SetInt(static_cast<int>(error));
wxPostEvent(m_owner, event);
}
NativeMediaCtrl::NativeMediaCtrl(wxWindow* parent)
: wxWindow(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize)
, m_retry_enabled(true)
, m_retry_count(0)
{
SetBackgroundColour(*wxBLACK);
m_retry_timer.SetOwner(this);
Bind(wxEVT_TIMER, &NativeMediaCtrl::OnRetryTimer, this, m_retry_timer.GetId());
HWND hwnd = (HWND)GetHandle();
m_impl = std::make_unique<Impl>(this, hwnd);
}
NativeMediaCtrl::~NativeMediaCtrl()
{
m_retry_timer.Stop();
}
bool NativeMediaCtrl::Load(const wxString& url)
{
if (!IsSupported(url)) {
BOOST_LOG_TRIVIAL(warning) << "NativeMediaCtrl: Unsupported URL format: " << url.ToStdString();
return false;
}
m_current_url = url;
ResetRetryState();
StreamType type = DetectStreamType(url);
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl: Loading " << StreamTypeToString(type).ToStdString()
<< " stream: " << url.ToStdString();
return m_impl->Load(url);
}
void NativeMediaCtrl::Play()
{
if (m_impl) {
m_impl->Play();
}
}
void NativeMediaCtrl::Stop()
{
m_retry_timer.Stop();
ResetRetryState();
if (m_impl) {
m_impl->Stop();
}
}
void NativeMediaCtrl::Pause()
{
if (m_impl) {
m_impl->Pause();
}
}
NativeMediaState NativeMediaCtrl::GetState() const
{
return m_impl ? m_impl->GetState() : NativeMediaState::Stopped;
}
wxSize NativeMediaCtrl::GetVideoSize() const
{
return m_impl ? m_impl->GetVideoSize() : wxSize(1920, 1080);
}
NativeMediaError NativeMediaCtrl::GetLastError() const
{
return m_impl ? m_impl->GetLastError() : NativeMediaError::None;
}
void NativeMediaCtrl::DoSetSize(int x, int y, int width, int height, int sizeFlags)
{
wxWindow::DoSetSize(x, y, width, height, sizeFlags);
if (m_impl && width > 0 && height > 0) {
m_impl->UpdateLayout(width, height);
}
}
}} // namespace Slic3r::GUI
#endif // _WIN32

View file

@ -13,6 +13,7 @@
#include "MsgDialog.hpp"
#include "slic3r/Utils/Http.hpp"
#include "libslic3r/Thread.hpp"
#include "libslic3r/AppConfig.hpp"
#include "DeviceErrorDialog.hpp"
#include "RecenterDialog.hpp"
@ -22,6 +23,7 @@
#include <wx/mstream.h>
#include <wx/sstream.h>
#include <wx/zstream.h>
#include <algorithm>
#include "DeviceCore/DevBed.h"
#include "DeviceCore/DevCtrl.h"
@ -1383,6 +1385,12 @@ StatusBasePanel::~StatusBasePanel()
delete m_custom_camera_view;
m_custom_camera_view = nullptr;
}
if (m_native_camera_ctrl) {
m_native_camera_ctrl->Stop();
delete m_native_camera_ctrl;
m_native_camera_ctrl = nullptr;
}
}
void StatusBasePanel::init_bitmaps()
@ -1495,10 +1503,12 @@ wxBoxSizer *StatusBasePanel::create_monitoring_page()
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);
if (m_custom_camera_view && m_custom_camera_view->IsShown()) {
const std::string js_request_pip = R"(
document.querySelector('video').requestPictureInPicture();
)";
m_custom_camera_view->RunScript(js_request_pip);
}
});
m_camera_switch_button->Hide();
@ -1532,13 +1542,9 @@ 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_media_play_ctrl = new MediaPlayCtrl(this, m_media_ctrl, wxDefaultPosition, wxSize(-1, FromDIP(40)));
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") {
@ -1546,27 +1552,90 @@ wxBoxSizer *StatusBasePanel::create_monitoring_page()
}
});
m_native_camera_ctrl = new NativeMediaCtrl(this);
m_native_camera_ctrl->SetMinSize(wxSize(PAGE_MIN_WIDTH, FromDIP(288)));
m_native_camera_ctrl->Hide();
m_native_camera_ctrl->Bind(EVT_NATIVE_MEDIA_STATE_CHANGED, &StatusBasePanel::on_native_camera_state_changed, this);
m_native_camera_ctrl->Bind(EVT_NATIVE_MEDIA_ERROR, &StatusBasePanel::on_native_camera_error, this);
m_native_camera_ctrl->Bind(EVT_NATIVE_MEDIA_SIZE_CHANGED, &StatusBasePanel::on_native_camera_size_changed, this);
m_media_play_ctrl = new MediaPlayCtrl(this, m_media_ctrl, wxDefaultPosition, wxSize(-1, FromDIP(40)));
sizer->Add(m_media_ctrl, 1, wxEXPAND | wxALL, 0);
sizer->Add(m_custom_camera_view, 1, wxEXPAND | wxALL, 0);
sizer->Add(m_native_camera_ctrl, 0, wxEXPAND | wxALL, 0);
sizer->Add(m_media_play_ctrl, 0, wxEXPAND | wxALL, 0);
// media_ctrl_panel->SetSizer(bSizer_monitoring);
// media_ctrl_panel->Layout();
//
// 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;
}
void StatusBasePanel::on_webview_navigating(wxWebViewEvent& evt) {
wxGetApp().CallAfter([this] {
remove_controls();
remove_webview_controls();
});
}
void StatusBasePanel::remove_webview_controls()
{
const std::string js_cleanup_video_element = R"(
document.body.style.overflow='hidden';
const video = document.querySelector('video');
if (video) {
video.setAttribute('style', 'width: 100% !important;');
video.removeAttribute('controls');
video.addEventListener('leavepictureinpicture', () => {
window.wx.postMessage('leavepictureinpicture');
});
video.addEventListener('enterpictureinpicture', () => {
window.wx.postMessage('enterpictureinpicture');
});
}
)";
m_custom_camera_view->RunScript(js_cleanup_video_element);
}
void StatusBasePanel::on_native_camera_state_changed(wxCommandEvent& event)
{
NativeMediaState state = static_cast<NativeMediaState>(event.GetInt());
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl state changed: " << static_cast<int>(state);
}
void StatusBasePanel::on_native_camera_error(wxCommandEvent& event)
{
NativeMediaError error = static_cast<NativeMediaError>(event.GetInt());
BOOST_LOG_TRIVIAL(warning) << "NativeMediaCtrl error: " << static_cast<int>(error);
}
void StatusBasePanel::on_native_camera_size_changed(wxCommandEvent& event)
{
if (!m_native_camera_ctrl) return;
wxSize video_size = m_native_camera_ctrl->GetVideoSize();
if (video_size.GetWidth() <= 0 || video_size.GetHeight() <= 0) return;
int container_width = m_native_camera_ctrl->GetSize().GetWidth();
if (container_width <= 0) {
container_width = m_media_ctrl ? m_media_ctrl->GetSize().GetWidth() : 0;
}
if (container_width <= 0) container_width = PAGE_MIN_WIDTH;
double aspect = (double)video_size.GetHeight() / (double)video_size.GetWidth();
int new_height = (int)(container_width * aspect);
m_native_camera_ctrl->SetMinSize(wxSize(-1, new_height));
m_native_camera_ctrl->SetMaxSize(wxSize(-1, new_height));
m_native_camera_ctrl->SetSize(-1, new_height);
wxWindow* parent = m_native_camera_ctrl->GetParent();
if (parent) {
parent->Layout();
parent->Refresh();
}
BOOST_LOG_TRIVIAL(info) << "NativeMediaCtrl resized to fit " << video_size.GetWidth() << "x" << video_size.GetHeight()
<< " -> container width: " << container_width << ", height: " << new_height;
}
wxBoxSizer *StatusBasePanel::create_machine_control_page(wxWindow *parent)
{
wxBoxSizer *bSizer_right = new wxBoxSizer(wxVERTICAL);
@ -2241,6 +2310,12 @@ void StatusPanel::update_camera_state(MachineObject* obj)
{
if (!obj) return;
std::string current_dev_id = obj->get_dev_id();
if (m_last_dev_id != current_dev_id) {
m_last_dev_id = current_dev_id;
handle_camera_source_change();
}
//sdcard
auto sdcard_state = obj->GetStorage()->get_sdcard_state();
if (m_last_sdcard != sdcard_state) {
@ -4842,7 +4917,7 @@ void StatusPanel::on_camera_enter(wxMouseEvent& event)
pos.y += sz.y;
m_camera_popup->SetPosition(pos);
m_camera_popup->update(m_media_play_ctrl->IsStreaming());
m_camera_popup->Popup();
m_camera_popup->Show();
}
}
@ -4853,14 +4928,55 @@ void StatusBasePanel::on_camera_source_change(wxCommandEvent& event)
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";
std::string new_cam_url;
CameraSourceType source_type = CameraSourceType::Builtin;
bool enabled = false;
if (enabled && !new_cam_url.empty()) {
m_custom_camera_view->LoadURL(new_cam_url);
toggle_custom_camera();
auto* dev_manager = wxGetApp().getDeviceManager();
MachineObject* machine = dev_manager ? dev_manager->get_selected_machine() : nullptr;
if (machine) {
std::string dev_id = machine->get_dev_id();
if (wxGetApp().app_config->has_printer_camera(dev_id)) {
auto config = wxGetApp().app_config->get_printer_camera(dev_id);
new_cam_url = config.custom_source;
source_type = config.source_type;
enabled = config.enabled;
}
}
wxSizer* sizer = m_media_ctrl->GetContainingSizer();
if (enabled && source_type != CameraSourceType::Builtin) {
if (source_type == CameraSourceType::RTSP || source_type == CameraSourceType::MJPEG) {
if (sizer) {
sizer->Show(m_custom_camera_view, false);
sizer->Show(m_media_ctrl, false);
sizer->Show(m_native_camera_ctrl, true);
}
m_custom_camera_view->Hide();
m_media_ctrl->Hide();
m_native_camera_ctrl->Load(new_cam_url);
m_native_camera_ctrl->Play();
m_native_camera_ctrl->Show();
} else {
if (sizer) {
sizer->Show(m_native_camera_ctrl, false);
sizer->Show(m_media_ctrl, false);
sizer->Show(m_custom_camera_view, true);
}
m_native_camera_ctrl->Stop();
m_native_camera_ctrl->Hide();
m_media_ctrl->Hide();
m_custom_camera_view->LoadURL(new_cam_url);
m_custom_camera_view->Show();
}
m_media_play_ctrl->Hide();
m_camera_switch_button->Show();
if (sizer) sizer->Layout();
} else {
m_native_camera_ctrl->Stop();
m_native_camera_ctrl->Hide();
m_custom_camera_view->Hide();
toggle_builtin_camera();
m_camera_switch_button->Hide();
}
@ -4868,25 +4984,80 @@ void StatusBasePanel::handle_camera_source_change()
void StatusBasePanel::toggle_builtin_camera()
{
wxSizer* sizer = m_media_ctrl->GetContainingSizer();
m_native_camera_ctrl->Stop();
m_native_camera_ctrl->Hide();
m_custom_camera_view->Hide();
m_media_ctrl->Show();
m_media_play_ctrl->Show();
if (sizer) {
sizer->Show(m_native_camera_ctrl, false);
sizer->Show(m_custom_camera_view, false);
sizer->Show(m_media_ctrl, true);
sizer->Layout();
}
}
void StatusBasePanel::toggle_custom_camera()
{
const auto enabled = wxGetApp().app_config->get("camera", "enable_custom_source") == "true";
std::string cam_url;
CameraSourceType source_type = CameraSourceType::Builtin;
bool enabled = false;
auto* dev_manager = wxGetApp().getDeviceManager();
MachineObject* machine = dev_manager ? dev_manager->get_selected_machine() : nullptr;
if (machine) {
std::string dev_id = machine->get_dev_id();
if (wxGetApp().app_config->has_printer_camera(dev_id)) {
auto config = wxGetApp().app_config->get_printer_camera(dev_id);
cam_url = config.custom_source;
source_type = config.source_type;
enabled = config.enabled;
}
}
if (enabled) {
m_custom_camera_view->Show();
wxSizer* sizer = m_media_ctrl->GetContainingSizer();
if (enabled && source_type != CameraSourceType::Builtin) {
m_media_ctrl->Hide();
m_media_play_ctrl->Hide();
if (source_type == CameraSourceType::RTSP || source_type == CameraSourceType::MJPEG) {
m_custom_camera_view->Hide();
m_native_camera_ctrl->Show();
if (sizer) {
sizer->Show(m_media_ctrl, false);
sizer->Show(m_custom_camera_view, false);
sizer->Show(m_native_camera_ctrl, true);
sizer->Layout();
}
} else {
m_native_camera_ctrl->Hide();
m_custom_camera_view->Show();
if (sizer) {
sizer->Show(m_media_ctrl, false);
sizer->Show(m_native_camera_ctrl, false);
sizer->Show(m_custom_camera_view, true);
sizer->Layout();
}
}
}
}
void StatusBasePanel::on_camera_switch_toggled(wxMouseEvent& event)
{
const auto enabled = wxGetApp().app_config->get("camera", "enable_custom_source") == "true";
bool enabled = false;
auto* dev_manager = wxGetApp().getDeviceManager();
MachineObject* machine = dev_manager ? dev_manager->get_selected_machine() : nullptr;
if (machine) {
std::string dev_id = machine->get_dev_id();
if (wxGetApp().app_config->has_printer_camera(dev_id)) {
auto config = wxGetApp().app_config->get_printer_camera(dev_id);
enabled = config.enabled;
}
}
if (enabled && m_media_ctrl->IsShown()) {
toggle_custom_camera();
} else {
@ -4894,27 +5065,10 @@ void StatusBasePanel::on_camera_switch_toggled(wxMouseEvent& event)
}
}
void StatusBasePanel::remove_controls()
{
const std::string js_cleanup_video_element = R"(
document.body.style.overflow='hidden';
const video = document.querySelector('video');
video.setAttribute('style', 'width: 100% !important;');
video.removeAttribute('controls');
video.addEventListener('leavepictureinpicture', () => {
window.wx.postMessage('leavepictureinpicture');
});
video.addEventListener('enterpictureinpicture', () => {
window.wx.postMessage('enterpictureinpicture');
});
)";
m_custom_camera_view->RunScript(js_cleanup_video_element);
}
void StatusPanel::on_camera_leave(wxMouseEvent& event)
{
if (obj && m_camera_popup) {
m_camera_popup->Dismiss();
m_camera_popup->Hide();
}
}

View file

@ -16,6 +16,7 @@
#include <wx/webrequest.h>
#include "wxMediaCtrl2.h"
#include "MediaPlayCtrl.h"
#include "NativeMediaCtrl.h"
#include "AMSSetting.hpp"
#include "Calibration.hpp"
#include "CalibrationWizardPage.hpp"
@ -460,7 +461,8 @@ protected:
ScalableButton *m_button_pause_resume;
ScalableButton *m_button_abort;
Button * m_button_clean;
wxWebView * m_custom_camera_view{nullptr};
wxWebView* m_custom_camera_view{nullptr};
NativeMediaCtrl* m_native_camera_ctrl{nullptr};
wxSimplebook* m_extruder_book;
std::vector<ExtruderImage *> m_extruderImage;
@ -572,11 +574,14 @@ protected:
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();
void on_native_camera_state_changed(wxCommandEvent& event);
void on_native_camera_error(wxCommandEvent& event);
void on_native_camera_size_changed(wxCommandEvent& event);
void on_webview_navigating(wxWebViewEvent& evt);
void remove_webview_controls();
public:
StatusBasePanel(wxWindow * parent,
@ -651,6 +656,7 @@ protected:
int m_last_timelapse = -1;
int m_last_extrusion = -1;
int m_last_vcamera = -1;
std::string m_last_dev_id;
int m_model_mall_request_count = 0;
bool m_is_load_with_temp = false;
json m_rating_result;

View file

@ -19,6 +19,7 @@ add_executable(${_TEST_NAME}_tests
test_timeutils.cpp
test_voronoi.cpp
test_optimizers.cpp
test_printer_camera_config.cpp
# test_png_io.cpp
test_indexed_triangle_set.cpp
../libnest2d/printer_parts.cpp

View file

@ -0,0 +1,347 @@
#include <catch2/catch_all.hpp>
#include "libslic3r/AppConfig.hpp"
using namespace Slic3r;
SCENARIO("Printer camera configuration storage", "[Config][Camera]") {
GIVEN("An AppConfig instance") {
AppConfig config;
WHEN("No printer camera is configured") {
THEN("has_printer_camera returns false") {
REQUIRE_FALSE(config.has_printer_camera("dev123"));
}
THEN("get_printer_camera returns empty config") {
auto cam = config.get_printer_camera("dev123");
REQUIRE(cam.dev_id.empty());
REQUIRE(cam.custom_source.empty());
REQUIRE_FALSE(cam.enabled);
}
THEN("get_all_printer_cameras returns empty map") {
REQUIRE(config.get_all_printer_cameras().empty());
}
}
WHEN("A printer camera is configured") {
PrinterCameraConfig cam_config;
cam_config.dev_id = "ABC123";
cam_config.custom_source = "http://camera.local/stream";
cam_config.enabled = true;
config.set_printer_camera(cam_config);
THEN("has_printer_camera returns true") {
REQUIRE(config.has_printer_camera("ABC123"));
}
THEN("get_printer_camera returns correct values") {
auto cam = config.get_printer_camera("ABC123");
REQUIRE(cam.dev_id == "ABC123");
REQUIRE(cam.custom_source == "http://camera.local/stream");
REQUIRE(cam.enabled == true);
}
THEN("Other printers are unaffected") {
REQUIRE_FALSE(config.has_printer_camera("XYZ789"));
}
THEN("get_all_printer_cameras contains the config") {
auto all = config.get_all_printer_cameras();
REQUIRE(all.size() == 1);
REQUIRE(all.count("ABC123") == 1);
}
}
WHEN("A printer camera is configured with enabled=false") {
PrinterCameraConfig cam_config;
cam_config.dev_id = "DEV456";
cam_config.custom_source = "rtsp://camera.local:554/stream";
cam_config.enabled = false;
config.set_printer_camera(cam_config);
THEN("has_printer_camera returns true") {
REQUIRE(config.has_printer_camera("DEV456"));
}
THEN("get_printer_camera returns enabled=false") {
auto cam = config.get_printer_camera("DEV456");
REQUIRE(cam.enabled == false);
}
}
WHEN("Multiple printer cameras are configured") {
PrinterCameraConfig cam1;
cam1.dev_id = "dev1";
cam1.custom_source = "http://cam1";
cam1.enabled = true;
PrinterCameraConfig cam2;
cam2.dev_id = "dev2";
cam2.custom_source = "http://cam2";
cam2.enabled = false;
PrinterCameraConfig cam3;
cam3.dev_id = "dev3";
cam3.custom_source = "rtsp://cam3:554/live";
cam3.enabled = true;
config.set_printer_camera(cam1);
config.set_printer_camera(cam2);
config.set_printer_camera(cam3);
THEN("get_all_printer_cameras returns all configs") {
auto all = config.get_all_printer_cameras();
REQUIRE(all.size() == 3);
REQUIRE(all.count("dev1") == 1);
REQUIRE(all.count("dev2") == 1);
REQUIRE(all.count("dev3") == 1);
}
THEN("Each config can be retrieved independently") {
REQUIRE(config.get_printer_camera("dev1").custom_source == "http://cam1");
REQUIRE(config.get_printer_camera("dev2").custom_source == "http://cam2");
REQUIRE(config.get_printer_camera("dev3").custom_source == "rtsp://cam3:554/live");
}
THEN("Each config preserves its enabled state") {
REQUIRE(config.get_printer_camera("dev1").enabled == true);
REQUIRE(config.get_printer_camera("dev2").enabled == false);
REQUIRE(config.get_printer_camera("dev3").enabled == true);
}
}
WHEN("A printer camera is erased") {
PrinterCameraConfig cam;
cam.dev_id = "dev_to_delete";
cam.custom_source = "http://will_be_deleted";
cam.enabled = true;
config.set_printer_camera(cam);
REQUIRE(config.has_printer_camera("dev_to_delete"));
config.erase_printer_camera("dev_to_delete");
THEN("has_printer_camera returns false") {
REQUIRE_FALSE(config.has_printer_camera("dev_to_delete"));
}
THEN("get_printer_camera returns empty config") {
auto result = config.get_printer_camera("dev_to_delete");
REQUIRE(result.dev_id.empty());
}
}
WHEN("Erasing a non-existent printer camera") {
config.erase_printer_camera("nonexistent_dev");
THEN("No exception is thrown and state remains consistent") {
REQUIRE_FALSE(config.has_printer_camera("nonexistent_dev"));
REQUIRE(config.get_all_printer_cameras().empty());
}
}
WHEN("A printer camera config is updated") {
PrinterCameraConfig cam;
cam.dev_id = "dev_update";
cam.custom_source = "http://old_url";
cam.enabled = false;
config.set_printer_camera(cam);
cam.custom_source = "http://new_url";
cam.enabled = true;
config.set_printer_camera(cam);
THEN("New values are stored") {
auto result = config.get_printer_camera("dev_update");
REQUIRE(result.custom_source == "http://new_url");
REQUIRE(result.enabled == true);
}
THEN("Only one entry exists in the map") {
REQUIRE(config.get_all_printer_cameras().size() == 1);
}
}
WHEN("One of multiple cameras is erased") {
PrinterCameraConfig cam1;
cam1.dev_id = "keep1";
cam1.custom_source = "http://keep1";
cam1.enabled = true;
PrinterCameraConfig cam2;
cam2.dev_id = "delete_me";
cam2.custom_source = "http://delete";
cam2.enabled = true;
PrinterCameraConfig cam3;
cam3.dev_id = "keep2";
cam3.custom_source = "http://keep2";
cam3.enabled = false;
config.set_printer_camera(cam1);
config.set_printer_camera(cam2);
config.set_printer_camera(cam3);
config.erase_printer_camera("delete_me");
THEN("Only the specified camera is removed") {
REQUIRE(config.has_printer_camera("keep1"));
REQUIRE_FALSE(config.has_printer_camera("delete_me"));
REQUIRE(config.has_printer_camera("keep2"));
}
THEN("Remaining cameras retain their values") {
REQUIRE(config.get_printer_camera("keep1").custom_source == "http://keep1");
REQUIRE(config.get_printer_camera("keep2").custom_source == "http://keep2");
}
THEN("Map size decreases by one") {
REQUIRE(config.get_all_printer_cameras().size() == 2);
}
}
}
}
SCENARIO("PrinterCameraConfig struct equality", "[Config][Camera]") {
GIVEN("Two PrinterCameraConfig instances") {
PrinterCameraConfig config1;
config1.dev_id = "test_dev";
config1.custom_source = "http://test";
config1.enabled = true;
PrinterCameraConfig config2;
config2.dev_id = "test_dev";
config2.custom_source = "http://test";
config2.enabled = true;
WHEN("All fields are identical") {
THEN("They are equal") {
REQUIRE(config1 == config2);
REQUIRE_FALSE(config1 != config2);
}
}
WHEN("dev_id differs") {
config2.dev_id = "different_dev";
THEN("They are not equal") {
REQUIRE_FALSE(config1 == config2);
REQUIRE(config1 != config2);
}
}
WHEN("custom_source differs") {
config2.custom_source = "http://different";
THEN("They are not equal") {
REQUIRE_FALSE(config1 == config2);
REQUIRE(config1 != config2);
}
}
WHEN("enabled differs") {
config2.enabled = false;
THEN("They are not equal") {
REQUIRE_FALSE(config1 == config2);
REQUIRE(config1 != config2);
}
}
}
}
SCENARIO("PrinterCameraConfig handles various URL formats", "[Config][Camera]") {
GIVEN("An AppConfig instance") {
AppConfig config;
WHEN("HTTP URL is stored") {
PrinterCameraConfig cam;
cam.dev_id = "http_test";
cam.custom_source = "http://192.168.1.100:8080/stream";
cam.enabled = true;
config.set_printer_camera(cam);
THEN("URL is preserved exactly") {
REQUIRE(config.get_printer_camera("http_test").custom_source == "http://192.168.1.100:8080/stream");
}
}
WHEN("HTTPS URL is stored") {
PrinterCameraConfig cam;
cam.dev_id = "https_test";
cam.custom_source = "https://secure.camera.com/live";
cam.enabled = true;
config.set_printer_camera(cam);
THEN("URL is preserved exactly") {
REQUIRE(config.get_printer_camera("https_test").custom_source == "https://secure.camera.com/live");
}
}
WHEN("RTSP URL is stored") {
PrinterCameraConfig cam;
cam.dev_id = "rtsp_test";
cam.custom_source = "rtsp://user:pass@camera.local:554/stream1";
cam.enabled = true;
config.set_printer_camera(cam);
THEN("URL is preserved exactly including credentials") {
REQUIRE(config.get_printer_camera("rtsp_test").custom_source == "rtsp://user:pass@camera.local:554/stream1");
}
}
WHEN("URL with special characters is stored") {
PrinterCameraConfig cam;
cam.dev_id = "special_test";
cam.custom_source = "http://camera.local/stream?quality=high&fps=30";
cam.enabled = true;
config.set_printer_camera(cam);
THEN("URL is preserved with query parameters") {
REQUIRE(config.get_printer_camera("special_test").custom_source == "http://camera.local/stream?quality=high&fps=30");
}
}
WHEN("Empty URL is stored") {
PrinterCameraConfig cam;
cam.dev_id = "empty_url_test";
cam.custom_source = "";
cam.enabled = false;
config.set_printer_camera(cam);
THEN("Empty URL is preserved") {
auto result = config.get_printer_camera("empty_url_test");
REQUIRE(result.custom_source.empty());
REQUIRE(result.enabled == false);
}
}
}
}
SCENARIO("PrinterCameraConfig handles various dev_id formats", "[Config][Camera]") {
GIVEN("An AppConfig instance") {
AppConfig config;
WHEN("Standard serial number is used") {
PrinterCameraConfig cam;
cam.dev_id = "01P00A123456789";
cam.custom_source = "http://test";
cam.enabled = true;
config.set_printer_camera(cam);
THEN("Serial number is stored correctly") {
REQUIRE(config.has_printer_camera("01P00A123456789"));
}
}
WHEN("UUID-style dev_id is used") {
PrinterCameraConfig cam;
cam.dev_id = "550e8400-e29b-41d4-a716-446655440000";
cam.custom_source = "http://test";
cam.enabled = true;
config.set_printer_camera(cam);
THEN("UUID is stored correctly") {
REQUIRE(config.has_printer_camera("550e8400-e29b-41d4-a716-446655440000"));
}
}
WHEN("Short dev_id is used") {
PrinterCameraConfig cam;
cam.dev_id = "ABC";
cam.custom_source = "http://test";
cam.enabled = true;
config.set_printer_camera(cam);
THEN("Short ID is stored correctly") {
REQUIRE(config.has_printer_camera("ABC"));
}
}
}
}