mirror of
https://github.com/SoftFever/OrcaSlicer.git
synced 2025-12-24 00:28:38 -07:00
Merge e070b4f55a into 506fde8f86
This commit is contained in:
commit
dc4529f1de
16 changed files with 3497 additions and 241 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
417
src/slic3r/GUI/CameraManagementDialog.cpp
Normal file
417
src/slic3r/GUI/CameraManagementDialog.cpp
Normal 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
|
||||
76
src/slic3r/GUI/CameraManagementDialog.hpp
Normal file
76
src/slic3r/GUI/CameraManagementDialog.hpp
Normal 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_
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
};
|
||||
|
||||
|
||||
|
|
|
|||
177
src/slic3r/GUI/NativeMediaCtrl.cpp
Normal file
177
src/slic3r/GUI/NativeMediaCtrl.cpp
Normal 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
|
||||
106
src/slic3r/GUI/NativeMediaCtrl.h
Normal file
106
src/slic3r/GUI/NativeMediaCtrl.h
Normal 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
|
||||
471
src/slic3r/GUI/NativeMediaCtrl_Linux.cpp
Normal file
471
src/slic3r/GUI/NativeMediaCtrl_Linux.cpp
Normal 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__
|
||||
619
src/slic3r/GUI/NativeMediaCtrl_Mac.mm
Normal file
619
src/slic3r/GUI/NativeMediaCtrl_Mac.mm
Normal 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
|
||||
876
src/slic3r/GUI/NativeMediaCtrl_Win.cpp
Normal file
876
src/slic3r/GUI/NativeMediaCtrl_Win.cpp
Normal 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,
|
||||
×tamp,
|
||||
&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
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
347
tests/libslic3r/test_printer_camera_config.cpp
Normal file
347
tests/libslic3r/test_printer_camera_config.cpp
Normal 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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue